Compare commits

..

1 Commits

Author SHA1 Message Date
Manuel Raynaud
561c72bec3 (backend) fix flaky search descendants test
One test about the search descendants test was flaky. It is because the
link_reach and link_role were used to test the
ancestors_link_(reach|role). The properties ancestors_link_reach and
ancestors_link_role should be used instead.
2026-04-29 09:05:46 +02:00
109 changed files with 4332 additions and 4862 deletions

View File

@@ -8,30 +8,8 @@ and this project adheres to
### Added
- ⚡️(frontend) add skeleton on content loading #2254
### Fixed
- 🐛(frontend) fix patch and comments #2273
- 🐛(frontend) interlinking are exported correctly in print mode #2269
- 💬(frontend) add missing link in onboarding description #2233
- 🐛(frontend) sanitize pasted and dropped content in document title #2210
- 🐛(frontend) Emoji menu doesn't display above comment box #2229
- 🐛(frontend) Block menu doesn't stay open on 1st line #2229
- 🐛(frontend) The "+" on the first line of a new doc doesn't work #2229
### Security
- 🔒️(frontend) sanitize color during collaboration #2270
## [v5.0.0] - 2026-04-08
### Added
- ✨(backend) create a dedicated endpoint to update document content #2171
- ⚡️(backend) stream s3 file content with a dedicated endpoint #2171
- ✨(backend) allow to use new ai feature using mistral sdk #2193
- ✨(backend) create a dedicated endpoint to update document content
- ⚡️(backend) stream s3 file content with a dedicated endpoint
### Changed
@@ -54,14 +32,11 @@ and this project adheres to
- 🐛(backend) load jwks url when OIDC_RS_PRIVATE_KEY_STR is set
- 🐛(backend) Prevent moving document to its own descendant or self #2208
- 🐛(backend) return 400 when restoring a non-deleted document #2225
- 🐛(backend) fix race condition in reconciliation requests CSV import #2153
- 🐛(backend) create_for_owner: add accesses before saving doc content #2124
- 🐛(backend) enforce emoji validation for reactions #1965
### Removed
- 🔥(backend) remove deprecated descendants endpoint #2243
- 🔥(backend) remove content in document responses #2171
- 🔥(backend) remove content in document responses
## [v4.8.6] - 2026-04-08
@@ -100,6 +75,7 @@ and this project adheres to
- ⚡️(frontend) add jitter to WS reconnection #2162
- 🐛(frontend) fix tree pagination #2145
- 🐛(nginx) add page reconciliation on nginx #2154
- 🐛(backend) fix race condition in reconciliation requests CSV import #2153
## [v4.8.4] - 2026-03-25
@@ -121,6 +97,9 @@ and this project adheres to
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
### Fixed
- 🐛(backend) create_for_owner: add accesses before saving doc content #2124
## [v4.8.3] - 2026-03-23
@@ -226,6 +205,7 @@ and this project adheres to
### Fixed
- 🐛(backend) enforce emoji validation for reactions #1965
- 🐛(frontend) analytic feature flags problem #1953
- 🐛(frontend) fix home collapsing panel #1954
- 🐛(frontend) fix disabled color on icon Dropdown #1950
@@ -1289,8 +1269,7 @@ 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/v5.0.0...main
[v5.0.0]: https://github.com/suitenumerique/docs/releases/v5.0.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.8.6...main
[v4.8.6]: https://github.com/suitenumerique/docs/releases/v4.8.6
[v4.8.5]: https://github.com/suitenumerique/docs/releases/v4.8.5
[v4.8.4]: https://github.com/suitenumerique/docs/releases/v4.8.4

View File

@@ -16,8 +16,6 @@ the following command inside your docker container:
## [Unreleased]
### [5.0.0] - 2026-04-30
We made several changes around document content management leading to several breaking changes in the API.
- The endpoint `/api/v1.0/documents/{document_id}/content/` has been renamed in `/api/v1.0/documents/{document_id}/formatted-content/`
@@ -35,9 +33,6 @@ Other changes:
- The deprecated endpoint `/api/v1.0/documents/<document_id>/descendants` is removed. The search endpoint should be used instead.
- Upgrade docspec dependency to version >= 3.0.0
The docspec service has changed since version 3.0.0, we ware now compatible with this version and not with version 2.x.x anymore
- It is now possible to use the Mistral SDK instead of the OpenAI for the AI features. If your provider is compatible with the mistral API, we encourage you to use it.
- `AI_API_KEY` settings is renamed in `OPENAI_SDK_API_KEY` and is only used to congiure the OpenAi sdk
- `AI_BASE_URL` settings is renamed in `OPENAI_SDK_BASE_URL` and is only used to congiure the OpenAi sdk
## [4.6.0] - 2026-02-27

View File

@@ -9,16 +9,14 @@ These are the environment variables you can set for the `impress-backend` contai
| Option | Description | default |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| OPENAI_SDK_API_KEY | AI key to be used by the OpenAI python SDK | |
| OPENAI_SDK_BASE_URL | OpenAI compatible AI base url | |
| MISTRAL_SDK_API_KEY | AI key to be used by the Mistral python SDK /!\ Mistral sdk can be used only in async mode with uvicorn /!\ | |
| MISTRAL_SDK_BASE_URL | Mistral compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" } |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" }
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_FEATURE_BLOCKNOTE_ENABLED | Enable Blocknote AI options | false |
| AI_FEATURE_LEGACY_ENABLED | Enable legacyAI options | true |
| AI_FEATURE_BLOCKNOTE_ENABLED | Enable Blocknote AI options | false |
| AI_FEATURE_LEGACY_ENABLED | Enable legacyAI options | true |
| AI_MODEL | AI Model to use | |
| AI_VERCEL_SDK_VERSION | The vercel AI SDK version used | 6 |
| AI_VERCEL_SDK_VERSION | The vercel AI SDK version used | 6 |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |

View File

@@ -71,6 +71,14 @@ OIDC_RS_ALLOWED_AUDIENCES=""
# User reconciliation
USER_RECONCILIATION_FORM_URL=http://localhost:3000
# AI
AI_FEATURE_ENABLED=true
AI_FEATURE_BLOCKNOTE_ENABLED=true
AI_FEATURE_LEGACY_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama
# Collaboration
COLLABORATION_API_URL=http://y-provider-development:4444/collaboration/api/
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000

View File

@@ -19,7 +19,7 @@ from rest_framework import serializers
from core import choices, enums, models, validators
from core.services import mime_types
from core.services.ai_services.legacy import AI_ACTIONS
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
Converter,

View File

@@ -49,8 +49,7 @@ from treebeard.exceptions import InvalidMoveToDescendant
from core import authentication, choices, enums, models
from core.api.filters import remove_accents
from core.services import mime_types
from core.services.ai_services.blocknote import AIService
from core.services.ai_services.legacy import get_legacy_ai_service
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.converter_services import (
ConversionError,
@@ -2134,16 +2133,13 @@ class DocumentViewSet(
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED or not settings.AI_FEATURE_LEGACY_ENABLED:
raise ValidationError("AI feature is not enabled.")
serializer = serializers.AITransformSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data["text"]
action = serializer.validated_data["action"]
response = get_legacy_ai_service().transform(text, action)
response = AIService().transform(text, action)
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
@@ -2165,16 +2161,13 @@ class DocumentViewSet(
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED or not settings.AI_FEATURE_LEGACY_ENABLED:
raise ValidationError("AI feature is not enabled.")
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data["text"]
language = serializer.validated_data["language"]
response = get_legacy_ai_service().translate(text, language)
response = AIService().translate(text, language)
return drf.response.Response(response, status=drf.status.HTTP_200_OK)

View File

@@ -7,17 +7,15 @@ import os
import queue
import threading
from collections.abc import AsyncIterator, Iterator
from functools import cache
from typing import Any, Dict, Union
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from langfuse import get_client
from langfuse.openai import OpenAI as OpenAI_Langfuse
from pydantic_ai import Agent, DeferredToolRequests
from pydantic_ai.models.mistral import MistralModel
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.mistral import MistralProvider
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.tools import ToolDefinition
from pydantic_ai.toolsets.external import ExternalToolset
@@ -26,6 +24,13 @@ from pydantic_ai.ui.vercel_ai import VercelAIAdapter
from pydantic_ai.ui.vercel_ai.request_types import RequestData, TextUIPart, UIMessage
from rest_framework.request import Request
from core import enums
if settings.LANGFUSE_PUBLIC_KEY:
OpenAI = OpenAI_Langfuse
else:
from openai import OpenAI
log = logging.getLogger(__name__)
BLOCKNOTE_TOOL_STRICT_PROMPT = """
@@ -59,6 +64,50 @@ IDs ALWAYS end with "$". Use ids EXACTLY as provided.
Return ONLY the JSON tool input. No prose, no markdown.
"""
AI_ACTIONS = {
"prompt": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"correct": (
"Correct grammar and spelling of the markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"rephrase": (
"Rephrase the given markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"summarize": (
"Summarize the markdown text, preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"beautify": (
"Add formatting to the text to make it more readable. "
"Do not provide any other information. "
"Preserve the language."
),
"emojify": (
"Add emojis to the important parts of the text. "
"Do not provide any other information. "
"Preserve the language."
),
}
AI_TRANSLATE = (
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
)
def convert_async_generator_to_sync(async_gen: AsyncIterator[str]) -> Iterator[str]:
"""Convert an async generator to a sync generator."""
@@ -94,41 +143,47 @@ def convert_async_generator_to_sync(async_gen: AsyncIterator[str]) -> Iterator[s
thread.join()
@cache
def configure_pydantic_model_provider() -> OpenAIChatModel | MistralModel:
"""Configure a pydantic Model and return it."""
if (
settings.OPENAI_SDK_API_KEY
and settings.OPENAI_SDK_BASE_URL
and settings.AI_MODEL
):
return OpenAIChatModel(
settings.AI_MODEL,
provider=OpenAIProvider(
api_key=settings.OPENAI_SDK_API_KEY,
base_url=settings.OPENAI_SDK_BASE_URL,
),
)
if (
settings.MISTRAL_SDK_API_KEY
and settings.MISTRAL_SDK_BASE_URL
and settings.AI_MODEL
):
return MistralModel(
settings.AI_MODEL,
provider=MistralProvider(
api_key=settings.MISTRAL_SDK_API_KEY,
base_url=settings.MISTRAL_SDK_BASE_URL,
),
)
raise ImproperlyConfigured("AI configuration not set")
class AIService:
"""Service class for AI-related operations."""
def __init__(self):
"""Ensure that the AI configuration is set properly."""
if (
settings.AI_BASE_URL is None
or settings.AI_API_KEY is None
or settings.AI_MODEL is None
):
raise ImproperlyConfigured("AI configuration not set")
self.client = OpenAI(base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY)
def call_ai_api(self, system_content, text):
"""Helper method to call the OpenAI API and process the response."""
response = self.client.chat.completions.create(
model=settings.AI_MODEL,
messages=[
{"role": "system", "content": system_content},
{"role": "user", "content": text},
],
)
content = response.choices[0].message.content
if not content:
raise RuntimeError("AI response does not contain an answer")
return {"answer": content}
def transform(self, text, action):
"""Transform text based on specified action."""
system_content = AI_ACTIONS[action]
return self.call_ai_api(system_content, text)
def translate(self, text, language):
"""Translate text to a specified language."""
language_display = enums.ALL_LANGUAGES.get(language, language)
system_content = AI_TRANSLATE.format(language=language_display)
return self.call_ai_api(system_content, text)
@staticmethod
def inject_document_state_messages(
messages: list[UIMessage],
@@ -269,9 +324,13 @@ class AIService:
langfuse.auth_check()
Agent.instrument_all()
agent = Agent(
configure_pydantic_model_provider(), instrument=instrument_enabled
model = OpenAIChatModel(
settings.AI_MODEL,
provider=OpenAIProvider(
base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY
),
)
agent = Agent(model, instrument=instrument_enabled)
accept = request.META.get("HTTP_ACCEPT", SSE_CONTENT_TYPE)

View File

@@ -1,201 +0,0 @@
"""Module dedicated to the legacy ai services."""
import logging
from abc import ABC, abstractmethod
from functools import cache
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from langfuse import get_client, observe
from langfuse.openai import OpenAI as OpenAI_Langfuse
from mistralai import Mistral
from core import enums
if settings.LANGFUSE_PUBLIC_KEY:
OpenAI = OpenAI_Langfuse
else:
from openai import OpenAI
log = logging.getLogger(__name__)
AI_ACTIONS = {
"prompt": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"correct": (
"Correct grammar and spelling of the markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"rephrase": (
"Rephrase the given markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"summarize": (
"Summarize the markdown text, preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"beautify": (
"Add formatting to the text to make it more readable. "
"Do not provide any other information. "
"Preserve the language."
),
"emojify": (
"Add emojis to the important parts of the text. "
"Do not provide any other information. "
"Preserve the language."
),
}
AI_TRANSLATE = (
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown delimiters."
)
class LegacyAiClient(ABC):
"""abstract class for legacy client."""
@abstractmethod
def call_ai_api(self, system_content, text) -> str:
"""Abstract method call_ai_api."""
class LegacyAiServiceMistralClient(LegacyAiClient):
"""ai_service using mistral sdk for the legacy ai feature."""
def __init__(self):
"""Configure mistral sdk"""
if (
not settings.MISTRAL_SDK_API_KEY
or not settings.MISTRAL_SDK_BASE_URL
or not settings.AI_MODEL
):
raise ImproperlyConfigured("Mistral sdk configuration not set")
self.client = Mistral(
api_key=settings.MISTRAL_SDK_API_KEY,
server_url=settings.MISTRAL_SDK_BASE_URL,
)
@observe(as_type="generation")
def call_ai_api(self, system_content, text) -> str:
langfuse = None
messages = [
{"role": "system", "content": system_content},
{"role": "user", "content": text},
]
if settings.LANGFUSE_PUBLIC_KEY:
langfuse = get_client()
langfuse.auth_check()
langfuse.update_current_generation(
input=messages,
model=settings.AI_MODEL,
)
response = self.client.chat.complete(
model=settings.AI_MODEL,
messages=messages,
stream=False,
)
if langfuse:
langfuse.update_current_generation(
usage_details={
"input": response.usage.prompt_tokens,
"output": response.usage.completion_tokens,
},
output=response.choices[0].message.content,
)
return response.choices[0].message.content
class LegacyAiServiceOpenAiClient(LegacyAiClient):
"""ai_service using OpenAI client for the legacy ai feature."""
def __init__(self):
"""configure OpenAI client."""
if (
not settings.OPENAI_SDK_BASE_URL
or not settings.OPENAI_SDK_API_KEY
or not settings.AI_MODEL
):
raise ImproperlyConfigured("OpenAI configuration not set")
self.client = OpenAI(
base_url=settings.OPENAI_SDK_BASE_URL, api_key=settings.OPENAI_SDK_API_KEY
)
def call_ai_api(self, system_content, text) -> str:
response = self.client.chat.completions.create(
model=settings.AI_MODEL,
messages=[
{"role": "system", "content": system_content},
{"role": "user", "content": text},
],
)
return response.choices[0].message.content
class LegacyAIService:
"""Legacy ai service used by transform and translate actions."""
def __init__(self, ai_client: LegacyAiClient):
"""Assign client to the service."""
self.ai_client = ai_client
def call_ai_api(self, system_content, text):
"""Helper method to call the OpenAI API and process the response."""
content = self.ai_client.call_ai_api(system_content, text)
if not content:
raise RuntimeError("AI response does not contain an answer")
return {"answer": content}
def transform(self, text, action):
"""Transform text based on specified action."""
system_content = AI_ACTIONS[action]
return self.call_ai_api(system_content, text)
def translate(self, text, language):
"""Translate text to a specified language."""
language_display = enums.ALL_LANGUAGES.get(language, language)
system_content = AI_TRANSLATE.format(language=language_display)
return self.call_ai_api(system_content, text)
@cache
def get_legacy_ai_service() -> LegacyAIService:
"""Helper responsible to correctly instantiate and configure legacy ai service."""
ai_client = None
if settings.MISTRAL_SDK_API_KEY:
ai_client = LegacyAiServiceMistralClient()
if settings.OPENAI_SDK_API_KEY:
ai_client = LegacyAiServiceOpenAiClient()
if not ai_client:
raise ImproperlyConfigured(
"trying to configure legacy ai_service but missing client configuration."
)
return LegacyAIService(ai_client)

View File

@@ -11,7 +11,6 @@ import pytest
from rest_framework.test import APIClient
from core import factories
from core.services.ai_services.blocknote import configure_pydantic_model_provider
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@@ -21,14 +20,13 @@ pytestmark = pytest.mark.django_db
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://localhost-ai:12345/"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_BASE_URL = "http://localhost-ai:12345/"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
configure_pydantic_model_provider.cache_clear()
@override_settings(
@@ -67,7 +65,7 @@ def test_api_documents_ai_proxy_anonymous_forbidden(reach, role):
@override_settings(AI_ALLOW_REACH_FROM="public")
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_anonymous_success(mock_stream):
"""
Anonymous users should be able to request AI proxy to a document
@@ -151,7 +149,7 @@ def test_api_documents_ai_proxy_authenticated_forbidden(reach, role):
("public", "editor"),
],
)
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_authenticated_success(mock_stream, reach, role):
"""
Authenticated users should be able to request AI proxy to a document
@@ -207,7 +205,7 @@ def test_api_documents_ai_proxy_reader(via, mock_user_teams):
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_success(mock_stream, via, role, mock_user_teams):
"""Users with sufficient permissions should be able to request AI proxy."""
user = factories.UserFactory()
@@ -268,7 +266,7 @@ def test_api_documents_ai_proxy_ai_feature_disabled(settings, setting_to_disable
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_throttling_document(mock_stream):
"""
Throttling per document should be triggered on the AI proxy endpoint.
@@ -306,7 +304,7 @@ def test_api_documents_ai_proxy_throttling_document(mock_stream):
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_throttling_user(mock_stream):
"""
Throttling per user should be triggered on the AI proxy endpoint.
@@ -341,7 +339,7 @@ def test_api_documents_ai_proxy_throttling_user(mock_stream):
}
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_returns_streaming_response(mock_stream):
"""AI proxy should return a StreamingHttpResponse with correct headers."""
user = factories.UserFactory()

View File

@@ -2,62 +2,47 @@
Test AI transform API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.services.ai_services.legacy import get_legacy_ai_service
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture
def ai_settings(settings):
def ai_settings():
"""Fixture to set AI settings."""
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_MODEL = "llama"
@pytest.fixture(autouse=True)
def clear_openai_client_config():
"""Clear the _configure_legacy_openai_client cache"""
get_legacy_ai_service.cache_clear()
with override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="llama"
):
yield
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role, ai_allow_reach_from",
"reach, role",
[
("restricted", "reader", "public"),
("restricted", "reader", "authenticated"),
("restricted", "reader", "restricted"),
("restricted", "editor", "public"),
("restricted", "editor", "authenticated"),
("restricted", "editor", "restricted"),
("authenticated", "reader", "public"),
("authenticated", "reader", "authenticated"),
("authenticated", "reader", "restricted"),
("authenticated", "editor", "public"),
("authenticated", "editor", "authenticated"),
("authenticated", "editor", "restricted"),
("public", "reader", "public"),
("public", "reader", "authenticated"),
("public", "reader", "restricted"),
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_transform_anonymous_forbidden(
reach, role, ai_allow_reach_from, settings
):
def test_api_documents_ai_transform_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI transform if the link reach
and role don't allow it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
@@ -69,14 +54,14 @@ def test_api_documents_ai_transform_anonymous_forbidden(
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_success(mock_create, settings):
def test_api_documents_ai_transform_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = "public"
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
@@ -103,17 +88,14 @@ def test_api_documents_ai_transform_anonymous_success(mock_create, settings):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@pytest.mark.parametrize("ai_allow_reach_from", ["authenticated", "restricted"])
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_limited_by_setting(
mock_create, ai_allow_reach_from, settings
):
def test_api_documents_ai_transform_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
@@ -194,8 +176,8 @@ def test_api_documents_ai_transform_authenticated_success(mock_create, reach, ro
"role": "system",
"content": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters. Preserve the language and markdown formatting. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
@@ -271,8 +253,8 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te
"role": "system",
"content": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters. Preserve the language and markdown formatting. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
@@ -282,7 +264,6 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te
)
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_transform_empty_text():
"""The text should not be empty when requesting AI transform."""
user = factories.UserFactory()
@@ -299,7 +280,6 @@ def test_api_documents_ai_transform_empty_text():
assert response.json() == {"text": ["This field may not be blank."]}
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_transform_invalid_action():
"""The action should valid when requesting AI transform."""
user = factories.UserFactory()
@@ -316,14 +296,14 @@ def test_api_documents_ai_transform_invalid_action():
assert response.json() == {"action": ['"invalid" is not a valid choice.']}
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_throttling_document(mock_create, settings):
def test_api_documents_ai_transform_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
settings.AI_DOCUMENT_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
@@ -349,14 +329,14 @@ def test_api_documents_ai_transform_throttling_document(mock_create, settings):
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_throttling_user(mock_create, settings):
def test_api_documents_ai_transform_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
settings.AI_USER_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
user = factories.UserFactory()
client = APIClient()
client.force_login(user)

View File

@@ -2,32 +2,27 @@
Test AI translate API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.services.ai_services.legacy import get_legacy_ai_service
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture
def ai_settings(settings):
def ai_settings():
"""Fixture to set AI settings."""
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_MODEL = "llama"
@pytest.fixture(autouse=True)
def clear_openai_client_config():
"clear the configure_legacy_openai_client cache"
get_legacy_ai_service.cache_clear()
with override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="llama"
):
yield
def test_api_documents_ai_translate_viewset_options_metadata():
@@ -50,34 +45,24 @@ def test_api_documents_ai_translate_viewset_options_metadata():
}
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role, ai_allow_reach_from",
"reach, role",
[
("restricted", "reader", "public"),
("restricted", "reader", "authenticated"),
("restricted", "reader", "restricted"),
("restricted", "editor", "public"),
("restricted", "editor", "authenticated"),
("restricted", "editor", "restricted"),
("authenticated", "reader", "public"),
("authenticated", "reader", "authenticated"),
("authenticated", "reader", "restricted"),
("authenticated", "editor", "public"),
("authenticated", "editor", "authenticated"),
("authenticated", "editor", "restricted"),
("public", "reader", "public"),
("public", "reader", "authenticated"),
("public", "reader", "restricted"),
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_translate_anonymous_forbidden(
reach, role, ai_allow_reach_from, settings
):
def test_api_documents_ai_translate_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI translate if the link reach
and role don't allow it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
@@ -89,14 +74,14 @@ def test_api_documents_ai_translate_anonymous_forbidden(
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_success(mock_create, settings):
def test_api_documents_ai_translate_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = "public"
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
@@ -117,9 +102,7 @@ def test_api_documents_ai_translate_anonymous_success(mock_create, settings):
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -127,17 +110,14 @@ def test_api_documents_ai_translate_anonymous_success(mock_create, settings):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@pytest.mark.parametrize("ai_allow_reach_from", ["authenticated", "restricted"])
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_limited_by_setting(
mock_create, ai_allow_reach_from, settings
):
def test_api_documents_ai_translate_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
settings.AI_ALLOW_REACH_FROM = ai_allow_reach_from
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
@@ -221,9 +201,7 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -300,9 +278,7 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -310,7 +286,6 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
)
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_translate_empty_text():
"""The text should not be empty when requesting AI translate."""
user = factories.UserFactory()
@@ -327,7 +302,6 @@ def test_api_documents_ai_translate_empty_text():
assert response.json() == {"text": ["This field may not be blank."]}
@pytest.mark.usefixtures("ai_settings")
def test_api_documents_ai_translate_invalid_action():
"""The action should valid when requesting AI translate."""
user = factories.UserFactory()
@@ -344,14 +318,14 @@ def test_api_documents_ai_translate_invalid_action():
assert response.json() == {"language": ['"invalid" is not a valid choice.']}
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_throttling_document(mock_create, settings):
def test_api_documents_ai_translate_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI translate endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
settings.AI_DOCUMENT_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
@@ -377,14 +351,14 @@ def test_api_documents_ai_translate_throttling_document(mock_create, settings):
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_throttling_user(mock_create, settings):
def test_api_documents_ai_translate_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI translate endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
settings.AI_USER_RATE_THROTTLE_RATES = {"minute": 3, "hour": 6, "day": 10}
user = factories.UserFactory()
client = APIClient()
client.force_login(user)

View File

@@ -14,7 +14,6 @@ import pytest
from rest_framework.test import APIClient
from core import factories, models
from core.services.ai_services.legacy import get_legacy_ai_service
from core.tests.documents.test_api_documents_ai_proxy import ( # pylint: disable=unused-import
ai_settings,
)
@@ -24,12 +23,6 @@ pytestmark = pytest.mark.django_db
# pylint: disable=unused-argument
@pytest.fixture(autouse=True)
def clear_openai_client_config():
"""Clear the configure_legacy_openai_client cache."""
get_legacy_ai_service.cache_clear()
def test_external_api_documents_ai_transform_not_allowed(
user_token, resource_server_backend, user_specific_sub
):
@@ -226,9 +219,7 @@ def test_external_api_documents_ai_translate_can_be_allowed(
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information. "
"Return the content directly without wrapping it in code blocks or markdown "
"delimiters."
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
@@ -250,7 +241,7 @@ def test_external_api_documents_ai_translate_can_be_allowed(
}
)
@pytest.mark.usefixtures("ai_settings")
@patch("core.services.ai_services.blocknote.AIService.stream")
@patch("core.services.ai_services.AIService.stream")
def test_external_api_documents_ai_proxy_can_be_allowed(
mock_stream, user_token, resource_server_backend, user_specific_sub
):

View File

@@ -10,23 +10,14 @@ from django.core.exceptions import ImproperlyConfigured
from django.test.utils import override_settings
import pytest
from mistralai import Mistral
from openai import OpenAI, OpenAIError
from pydantic_ai.models.mistral import MistralModel
from pydantic_ai.models.openai import OpenAIChatModel
from openai import OpenAIError
from pydantic_ai.ui.vercel_ai.request_types import TextUIPart, UIMessage
from core.services.ai_services.blocknote import (
from core.services.ai_services import (
BLOCKNOTE_TOOL_STRICT_PROMPT,
AIService,
configure_pydantic_model_provider,
convert_async_generator_to_sync,
)
from core.services.ai_services.legacy import (
LegacyAiServiceMistralClient,
LegacyAiServiceOpenAiClient,
get_legacy_ai_service,
)
pytestmark = pytest.mark.django_db
@@ -35,129 +26,35 @@ pytestmark = pytest.mark.django_db
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
settings.AI_BASE_URL = "http://example.com"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
yield
configure_pydantic_model_provider.cache_clear()
get_legacy_ai_service.cache_clear()
# -- AIService configure sdk--
# -- AIService.__init__ --
@pytest.mark.parametrize(
"setting_name, setting_value",
[
("OPENAI_SDK_BASE_URL", None),
("OPENAI_SDK_API_KEY", None),
("AI_BASE_URL", None),
("AI_API_KEY", None),
("AI_MODEL", None),
],
)
def test_ai_services_configure_open_ai_leagcy_client_missing_settings(
setting_name, setting_value, settings
):
"""
An exception must be raised if an expected settings is missing to configure the openai sdk.
"""
def test_services_ai_setting_missing(setting_name, setting_value, settings):
"""Setting should be set"""
setattr(settings, setting_name, setting_value)
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
LegacyAiServiceOpenAiClient()
def test_ai_services_configure_open_ai_leagcy_client(settings):
"""With all required settings the OpenAi legacy client should be configured."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
legacy_openai_client = LegacyAiServiceOpenAiClient()
assert isinstance(legacy_openai_client.client, OpenAI)
@pytest.mark.parametrize(
"setting_name, setting_value",
[
("MISTRAL_SDK_BASE_URL", None),
("MISTRAL_SDK_API_KEY", None),
("AI_MODEL", None),
],
)
def test_ai_services_configure_mistral_sdk_leagcy_client_missing_settings(
setting_name, setting_value, settings
):
"""
An exception must be raised if an expected settings is missing to configure the openai sdk.
"""
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
setattr(settings, setting_name, setting_value)
with pytest.raises(
ImproperlyConfigured,
match="Mistral sdk configuration not set",
):
LegacyAiServiceMistralClient()
def test_ai_services_configure_mistral_sdk_legacy_client(settings):
"""With all required settings the Mistral sdk legacy client should be configured."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
settings.MISTRAL_SDK_API_KEY = "mistreal-sdk-key"
settings.MISTRAL_SDK_BASE_URL = "https://mistral.base-url.com"
legacy_mistral_client = LegacyAiServiceMistralClient()
assert isinstance(legacy_mistral_client.client, Mistral)
def test_ai_services_configure_pydantic_ai_model_openai(settings):
"""When openai sdk settings are configured it should return an OpenAiChatModel."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = "http://example.com"
settings.OPENAI_SDK_API_KEY = "test-key"
pydantic_ai_model = configure_pydantic_model_provider()
assert isinstance(pydantic_ai_model, OpenAIChatModel)
def test_ai_services_configure_pydantic_ai_model_mistral(settings):
"""When mistral sdk settings are configured is should return a MistralModel."""
settings.AI_MODEL = "llama"
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
settings.MISTRAL_SDK_API_KEY = "mistreal-sdk-key"
settings.MISTRAL_SDK_BASE_URL = "https://mistral.base-url.com"
pydantic_ai_model = configure_pydantic_model_provider()
assert isinstance(pydantic_ai_model, MistralModel)
def test_ai_services_configure_pydantic_ai_model_no_settings(settings):
"""When no settings are configured for a ai sdk it should raises an exception."""
settings.AI_MODEL = None
settings.OPENAI_SDK_BASE_URL = None
settings.OPENAI_SDK_API_KEY = None
settings.MISTRAL_SDK_API_KEY = None
settings.MISTRAL_SDK_BASE_URL = None
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
configure_pydantic_model_provider()
AIService()
# -- AIService.transform --
@@ -176,7 +73,7 @@ def test_services_ai_client_error(mock_create):
OpenAIError,
match="Mocked client error",
):
get_legacy_ai_service().transform("hello", "prompt")
AIService().transform("hello", "prompt")
@override_settings(
@@ -194,7 +91,7 @@ def test_services_ai_client_invalid_response(mock_create):
RuntimeError,
match="AI response does not contain an answer",
):
get_legacy_ai_service().transform("hello", "prompt")
AIService().transform("hello", "prompt")
@override_settings(
@@ -208,7 +105,7 @@ def test_services_ai_success(mock_create):
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
response = get_legacy_ai_service().transform("hello", "prompt")
response = AIService().transform("hello", "prompt")
assert response == {"answer": "Salut"}
@@ -224,7 +121,7 @@ def test_services_ai_translate_success(mock_create):
choices=[MagicMock(message=MagicMock(content="Bonjour"))]
)
response = get_legacy_ai_service().translate("<p>Hello</p>", "fr")
response = AIService().translate("<p>Hello</p>", "fr")
assert response == {"answer": "Bonjour"}
call_args = mock_create.call_args
@@ -240,7 +137,7 @@ def test_services_ai_translate_unknown_language(mock_create):
choices=[MagicMock(message=MagicMock(content="Translated"))]
)
response = get_legacy_ai_service().translate("<p>Hello</p>", "xx-unknown")
response = AIService().translate("<p>Hello</p>", "xx-unknown")
assert response == {"answer": "Translated"}
call_args = mock_create.call_args
@@ -551,7 +448,7 @@ def test_services_ai_stream_defaults_to_sync(mock_build, monkeypatch):
# -- AIService._build_async_stream --
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream(mock_adapter_cls):
"""_build_async_stream should build the pydantic-ai streaming pipeline."""
@@ -580,7 +477,7 @@ def test_services_ai_build_async_stream(mock_adapter_cls):
mock_adapter_instance.encode_stream.assert_called_once()
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_with_tool_definitions(mock_adapter_cls):
"""_build_async_stream should build an ExternalToolset when
toolDefinitions are present in the request."""
@@ -617,7 +514,7 @@ def test_services_ai_build_async_stream_with_tool_definitions(mock_adapter_cls):
assert len(call_kwargs["toolsets"]) == 1
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_with_tool_definitions_required_system_prompt(
mock_adapter_cls,
):
@@ -660,8 +557,8 @@ def test_services_ai_build_async_stream_with_tool_definitions_required_system_pr
assert mock_run_input.messages[0].parts[0].text == BLOCKNOTE_TOOL_STRICT_PROMPT
@patch("core.services.ai_services.blocknote.Agent")
@patch("core.services.ai_services.blocknote.VercelAIAdapter")
@patch("core.services.ai_services.Agent")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_langfuse_enabled(
mock_adapter_cls, mock_agent_cls, settings
):

View File

@@ -161,8 +161,7 @@
},
"onboarding": {
"enabled": true,
"learn_more_url": "",
"ready_template_url": ""
"learn_more_url": ""
},
"help": {
"documentation_url": ""

View File

@@ -808,30 +808,8 @@ class Base(Configuration):
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
MISTRAL_SDK_BASE_URL = values.Value(
None, environ_name="MISTRAL_SDK_BASE_URL", environ_prefix=None
)
MISTRAL_SDK_API_KEY = SecretFileValue(
None, environ_name="MISTRAL_SDK_API_KEY", environ_prefix=None
)
OPENAI_SDK_API_KEY = SecretFileValue(
default=SecretFileValue( # retrocompatibility
None,
environ_name="AI_API_KEY",
environ_prefix=None,
),
environ_name="OPENAI_SDK_API_KEY",
environ_prefix=None,
)
OPENAI_SDK_BASE_URL = values.Value(
default=values.Value( # retrocompatibility
None, environ_name="AI_BASE_URL", environ_prefix=None
),
environ_name="OPENAI_SDK_BASE_URL",
environ_prefix=None,
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_BOT = values.DictValue(
default={
"name": _("Docs AI"),
@@ -1171,11 +1149,6 @@ class Base(Configuration):
}
)
if cls.OPENAI_SDK_API_KEY and cls.MISTRAL_SDK_API_KEY:
raise ValueError(
"Both OPENAI_SDK and MISTRAL_SDK parameters can not be set simultaneously."
)
class Build(Base):
"""Settings used when the application is built.

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Titouroù personel"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Aotreoù"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Deiziadoù a-bouez"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Gwezennadur"
@@ -62,24 +62,24 @@ msgstr "Kuzhet"
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
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:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -375,151 +375,151 @@ msgstr "Restr"
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Digeriñ"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: 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:1495 core/models.py:1495
#: 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:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: 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:1724 core/models.py:1724
#: 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:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Berechtigungen"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Wichtige Termine"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Import-Job erstellt und in der Warteschlange."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Baumstruktur"
@@ -62,24 +62,24 @@ msgstr "Maskiert"
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Dies ist ein Pflichtfeld."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Der Zugriff auf den Link '%(link_reach)s' ist aufgrund der Konfiguration übergeordneter Dokumente nicht erlaubt."
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -149,15 +149,15 @@ msgstr "Rechts"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "ID"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "Primärschlüssel für den Datensatz als UUID"
msgstr "primärer Schlüssel für den Datensatz als UUID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "Erstellt am"
msgstr "Erstellt"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
@@ -375,151 +375,151 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Öffnen"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Dokument um Zugriff bitten"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Dokumentenabfragen"
#: build/lib/core/models.py:1730 core/models.py:1730
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Dieser Benutzer hat bereits um Zugang zu diesem Dokument gebeten."
#: build/lib/core/models.py:1787 core/models.py:1787
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} möchte Zugriff auf ein Dokument erhalten!"
#: build/lib/core/models.py:1791 core/models.py:1791
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} möchte auf das folgende Dokument zugreifen:"
#: build/lib/core/models.py:1797 core/models.py:1797
#: 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} bittet um Zugang zum Dokument: {title}"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Thread"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Threads"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Gast"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Kommentar"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Kommentare"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reaktion"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reaktionen"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Προσωπικές πληροφορίες"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: 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:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Σημαντικές ημερομηνίες"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Η εργασία εισαγωγής δημιουργήθηκε και μπήκε στην ουρά."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Επεξεργασία επιλεγμένων συμφωνιών χρηστών"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Δομή δέντρου"
@@ -62,24 +62,24 @@ msgstr "Με κάλυψη"
msgid "Favorite"
msgstr "Αγαπημένο"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ένα νέο έγγραφο δημιουργήθηκε εκ μέρους σας!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Σας παραχωρήθηκε η ιδιοκτησία ενός νέου εγγράφου:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Αυτό το πεδίο είναι υποχρεωτικό."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, 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:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "αντίγραφο του {title}"
@@ -382,151 +382,151 @@ msgstr "Έγγραφο"
msgid "Documents"
msgstr "Έγγραφα"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Άνοιγμα"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Ίχνος συνδέσμου εγγράφου/χρήστη"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Ίχνη συνδέσμου εγγράφου/χρήστη"
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Αγαπημένο έγγραφο"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Αγαπημένα έγγραφα"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Σχέση εγγράφου/χρήστη"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Σχέσεις εγγράφου/χρήστη"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Αυτός ο χρήστης συμμετέχει ήδη σε αυτό το έγγραφο."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Αυτή η ομάδα συμμετέχει ήδη σε αυτό το έγγραφο."
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Αίτημα πρόσβασης σε έγγραφο"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Αιτήματα πρόσβασης σε έγγραφα"
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Νήμα"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Νήματα"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Σχόλιο"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Σχόλια"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Αντίδραση"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Αντιδράσεις"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "διεύθυνση email"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Πρόσκληση σε έγγραφο"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Προσκλήσεις εγγράφου"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Τεχνητή Νοημοσύνη (AI) Docs"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: 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:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Información Personal"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permisos"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Fechas importantes"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Estructura en árbol"
@@ -62,24 +62,24 @@ msgstr "Enmascarado"
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -375,151 +375,151 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: 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:1495 core/models.py:1495
#: 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:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Thread"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Threads"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Anónimo"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Comentario"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Comentarios"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reacción"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reacciones"
#: build/lib/core/models.py:1955 core/models.py:1955
#: 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:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permissions"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Tâche d'importation créée et mise en file d'attente."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Traiter les rapprochements de l'utilisateur sélectionné"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Arborescence"
@@ -62,24 +62,24 @@ msgstr "Masqué"
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, 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:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -382,151 +382,151 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Ouvrir"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: 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:1495 core/models.py:1495
#: 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:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: 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:1724 core/models.py:1724
#: 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:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Conversation"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Conversations"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Commentaire"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Commentaires"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Réaction"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Réactions"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs IA"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Informazioni personali"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permessi"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Date importanti"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Struttura ad albero"
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -375,151 +375,151 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Apri"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Persoonlijke informatie"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Machtigingen"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Belangrijke data"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Import taak gemaakt en in de wachtrij geplaatst."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Verwerk geselecteerde gebruikers samenvoeging"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Boomstructuur"
@@ -62,24 +62,24 @@ msgstr "Gemaskeerd"
msgid "Favorite"
msgstr "Favoriet"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, 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:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -382,151 +382,151 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Open"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: 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:1724 core/models.py:1724
#: 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:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Kanaal"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Kanalen"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Reactie"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Reacties"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reactie"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reacties"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Informações Pessoais"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permissões"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Datas importantes"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Estrutura de árvore"
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: 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:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Задание по импорту создано и поставлено в очередь."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Обработка выбранных пользовательских сверок"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Древовидная структура"
@@ -62,24 +62,24 @@ msgstr "Скрытый"
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, 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:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -382,151 +382,151 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Открыть"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Обсуждение"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Обсуждения"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Комментарий"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Комментарии"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Реакция"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Реакции"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs ИИ"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Osebni podatki"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Dovoljenja"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Pomembni datumi"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Drevesna struktura"
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
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:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Odpri"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: 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:1495 core/models.py:1495
#: 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:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1560 core/models.py:1560
#: 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:1566 core/models.py:1566
#: 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:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Personuppgifter"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Behörigheter"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Viktiga datum"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
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:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Öppna"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: 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:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
@@ -62,24 +62,24 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -375,151 +375,151 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: 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:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Завдання імпорту створено і поставлено в чергу."
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Обробити обрані узгодження користувача"
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Ієрархічна структура"
@@ -62,24 +62,24 @@ msgstr "Приховано"
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, 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:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -382,151 +382,151 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Відкрити"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Обговорення"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Обговорення"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Коментар"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Коментарі"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Реакція"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Реакції"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs ШІ"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-30 12:37+0000\n"
"PO-Revision-Date: 2026-04-30 13:05\n"
"POT-Creation-Date: 2026-04-02 09:37+0000\n"
"PO-Revision-Date: 2026-04-08 13:28\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -17,28 +17,28 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "個人資訊"
#: build/lib/core/admin.py:46 build/lib/core/admin.py:166 core/admin.py:46
#: core/admin.py:166
#: 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:58 core/admin.py:58
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "重要日期"
#: build/lib/core/admin.py:117 core/admin.py:117
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:121 core/admin.py:121
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:176 core/admin.py:176
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "樹狀結構"
@@ -62,24 +62,24 @@ msgstr "已隱藏"
msgid "Favorite"
msgstr "我的最愛"
#: build/lib/core/api/serializers.py:507 core/api/serializers.py:507
#: build/lib/core/api/serializers.py:544 core/api/serializers.py:544
msgid "A new document was created on your behalf!"
msgstr "已代表您建立新文件!"
#: build/lib/core/api/serializers.py:511 core/api/serializers.py:511
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
msgid "You have been granted ownership of a new document:"
msgstr "您已獲得新文件的所有權:"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#: build/lib/core/api/serializers.py:584 core/api/serializers.py:584
msgid "This field is required."
msgstr "此欄位為必填。"
#: build/lib/core/api/serializers.py:558 core/api/serializers.py:558
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#, 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:1299 core/api/viewsets.py:1299
#: build/lib/core/api/viewsets.py:1312 core/api/viewsets.py:1312
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -375,151 +375,151 @@ msgstr "文件"
msgid "Documents"
msgstr "文件"
#: build/lib/core/models.py:940 build/lib/core/models.py:1347
#: core/models.py:940 core/models.py:1347
#: 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:1348 core/models.py:1348
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "開啟"
#: build/lib/core/models.py:1383 core/models.py:1383
#: 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:1387 core/models.py:1387
#: 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:1393 core/models.py:1393
#: 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:1494 core/models.py:1494
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:1495 core/models.py:1495
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:1501 core/models.py:1501
#: 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:1524 core/models.py:1524
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "文件收藏"
#: build/lib/core/models.py:1525 core/models.py:1525
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "文件收藏"
#: build/lib/core/models.py:1531 core/models.py:1531
#: 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:1553 core/models.py:1553
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1554 core/models.py:1554
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1560 core/models.py:1560
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "此使用者已在此文件中。"
#: build/lib/core/models.py:1566 core/models.py:1566
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "此團隊已在此文件中。"
#: build/lib/core/models.py:1572 core/models.py:1572
#: 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:1723 core/models.py:1723
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1724 core/models.py:1724
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1730 core/models.py:1730
#: 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:1787 core/models.py:1787
#: 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:1791 core/models.py:1791
#: 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:1797 core/models.py:1797
#: 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:1839 core/models.py:1839
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "對話串"
#: build/lib/core/models.py:1840 core/models.py:1840
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "對話串"
#: build/lib/core/models.py:1843 build/lib/core/models.py:1895
#: core/models.py:1843 core/models.py:1895
#: 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:1890 core/models.py:1890
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "評論"
#: build/lib/core/models.py:1891 core/models.py:1891
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "評論"
#: build/lib/core/models.py:1940 core/models.py:1940
#: 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:1944 core/models.py:1944
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "回應"
#: build/lib/core/models.py:1945 core/models.py:1945
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "回應"
#: build/lib/core/models.py:1955 core/models.py:1955
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "電子郵件地址"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "文件邀請"
#: build/lib/core/models.py:1975 core/models.py:1975
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "文件邀請"
#: build/lib/core/models.py:1995 core/models.py:1995
#: 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:837 impress/settings.py:837
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "5.0.0"
version = "4.8.6"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -26,7 +26,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.14.3",
"boto3==1.42.93",
"boto3==1.42.59",
"Brotli==1.2.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
@@ -34,39 +34,38 @@ dependencies = [
"django-countries==8.2.0",
"django-csp==4.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.26",
"django-lasuite[all]==0.0.24",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django<6.0.0",
"django-treebeard<5.0.0",
"djangorestframework==3.17.1",
"djangorestframework==3.16.1",
"django-waffle==5.0.0",
"drf_spectacular==0.29.0",
"dockerflow==2026.3.4",
"dockerflow==2026.1.26",
"easy_thumbnails==2.10.1",
"emoji==2.15.0",
"factory_boy==3.3.3",
"gunicorn==25.3.0",
"gunicorn==25.1.0",
"jsonschema==4.26.0",
"langfuse==3.11.2",
"lxml==6.1.0",
"markdown==3.10.2",
"mistralai==1.12.4",
"mozilla-django-oidc==5.0.2",
"nested-multipart-parser==1.6.0",
"openai==2.32.0",
"openai==2.24.0",
"psycopg[binary,pool]==3.3.3",
"pycrdt==0.12.50",
"pydantic==2.13.3",
"pycrdt==0.12.47",
"pydantic==2.12.5",
"pydantic-ai-slim[openai,logfire,web]==1.58.0",
"PyJWT==2.12.1",
"PyJWT==2.12.0",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.33.1",
"sentry-sdk==2.58.0",
"uvicorn==0.45.0",
"requests==2.33.0",
"sentry-sdk==2.53.0",
"uvicorn==0.41.0",
"whitenoise==6.12.0",
]
@@ -80,21 +79,21 @@ dependencies = [
dev = [
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2026.4.14",
"drf-spectacular-sidecar==2026.3.1",
"freezegun==1.5.5",
"ipdb==0.13.13",
"ipython==9.12.0",
"pyfakefs==6.2.0",
"ipython==9.10.0",
"pyfakefs==6.1.3",
"pylint-django==2.7.0",
"pylint<4.0.0",
"pytest-cov==7.1.0",
"pytest-cov==7.0.0",
"pytest-django==4.12.0",
"pytest==9.0.3",
"pytest==9.0.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.26.0",
"ruff==0.15.11",
"types-requests==2.33.0.20260408",
"ruff==0.15.4",
"types-requests==2.32.4.20260107",
]
[tool.setuptools]

View File

@@ -60,7 +60,7 @@ COPY --from=impress-builder /home/frontend/apps/impress/out /app
FROM ${FRONTEND_IMAGE} AS frontend-source
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.23 AS frontend-production
FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production
# Upgrade system packages to install security updates
USER root

File diff suppressed because one or more lines are too long

View File

@@ -192,10 +192,10 @@ endobj
(react-pdf)
endobj
55 0 obj
(D:20260505110445Z)
(D:20260403132357Z)
endobj
56 0 obj
(chromium-4903-0-doc-export-override-content)
(chromium-8651-0-doc-export-override-content)
endobj
52 0 obj
<<
@@ -216,7 +216,7 @@ endobj
58 0 obj
<<
/Type /FontDescriptor
/FontName /HRJUFI+Inter18pt-Regular
/FontName /VIBRRZ+Inter18pt-Regular
/Flags 4
/FontBBox [-742.1875 -323.242187 2579.589844 1109.375]
/ItalicAngle 0
@@ -232,7 +232,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /HRJUFI+Inter18pt-Regular
/BaseFont /VIBRRZ+Inter18pt-Regular
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -247,7 +247,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /HRJUFI+Inter18pt-Regular
/BaseFont /VIBRRZ+Inter18pt-Regular
/Encoding /Identity-H
/DescendantFonts [59 0 R]
/ToUnicode 60 0 R
@@ -256,7 +256,7 @@ endobj
62 0 obj
<<
/Type /FontDescriptor
/FontName /XKLDZR+Inter18pt-Bold
/FontName /TDKMKH+Inter18pt-Bold
/Flags 4
/FontBBox [-790.527344 -334.472656 2580.566406 1114.746094]
/ItalicAngle 0
@@ -272,7 +272,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /XKLDZR+Inter18pt-Bold
/BaseFont /TDKMKH+Inter18pt-Bold
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -287,7 +287,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /XKLDZR+Inter18pt-Bold
/BaseFont /TDKMKH+Inter18pt-Bold
/Encoding /Identity-H
/DescendantFonts [63 0 R]
/ToUnicode 64 0 R
@@ -296,7 +296,7 @@ endobj
66 0 obj
<<
/Type /FontDescriptor
/FontName /QHBJWW+Inter18pt-Italic
/FontName /JYBWBW+Inter18pt-Italic
/Flags 68
/FontBBox [-747.558594 -323.242187 2595.703125 1109.375]
/ItalicAngle -9.398804
@@ -312,7 +312,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /QHBJWW+Inter18pt-Italic
/BaseFont /JYBWBW+Inter18pt-Italic
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -327,7 +327,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /QHBJWW+Inter18pt-Italic
/BaseFont /JYBWBW+Inter18pt-Italic
/Encoding /Identity-H
/DescendantFonts [67 0 R]
/ToUnicode 68 0 R
@@ -336,7 +336,7 @@ endobj
70 0 obj
<<
/Type /FontDescriptor
/FontName /NBHLIK+GeistMono-Regular
/FontName /DLRHPN+GeistMono-Regular
/Flags 5
/FontBBox [-1738 -247 654 1012]
/ItalicAngle 0
@@ -352,7 +352,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /NBHLIK+GeistMono-Regular
/BaseFont /DLRHPN+GeistMono-Regular
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -367,7 +367,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /NBHLIK+GeistMono-Regular
/BaseFont /DLRHPN+GeistMono-Regular
/Encoding /Identity-H
/DescendantFonts [71 0 R]
/ToUnicode 72 0 R
@@ -376,7 +376,7 @@ endobj
74 0 obj
<<
/Type /FontDescriptor
/FontName /VMRKYJ+Inter18pt-BoldItalic
/FontName /LHWXUO+Inter18pt-BoldItalic
/Flags 68
/FontBBox [-795.898437 -334.472656 2596.191406 1114.746094]
/ItalicAngle -9.398804
@@ -392,7 +392,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /VMRKYJ+Inter18pt-BoldItalic
/BaseFont /LHWXUO+Inter18pt-BoldItalic
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -407,7 +407,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /VMRKYJ+Inter18pt-BoldItalic
/BaseFont /LHWXUO+Inter18pt-BoldItalic
/Encoding /Identity-H
/DescendantFonts [75 0 R]
/ToUnicode 76 0 R
@@ -713,21 +713,30 @@ endobj
/Filter /FlateDecode
>>
stream
xœí][<5B>㸱~ï_á?Ð
XôC²'Áž‡ Af439Ÿ`³ÀÉßIQÅ;eÙÛÝ[Û뱫,Ū<C385>deûäÿž±þG2<H…/Óõég÷‡íøBGó¿~ãGý—|ãgïoy]È@Ü»›ã<>ÏÚß}ÿåÿÿwúò—?ýþ2ýò„ì¿Lÿ÷ô»?ý‚/ÿøEŸõãzfâ"ÿÁ³ê½õA[þ#¹`rùðõéoß!„´ ú}¡ú%þ¬ÿõ3žŸ¹H>(̧s:(2
­B¼è«<EFBFBD>Ã82.…ÕÈ— úûåÃ?ý×}©ËÅ9 Á± Z².ûÙºmûó}„…<18>¢”Êùè7éCrº%F„ÕH[|HG6`ɹào؇_îáC.°cʇ<C38A>ȰŽâWçµ1Ù;:EW9€,<2C>ÔÓ“D—ëÓÈÑ@¤v§2ò·@.I“¬ÆºvÕ·XUWLæZó×—ñÉ]ó7÷ìÿ;=ýÏÓ_/›“ƒ J2Š© #Szøèñ0¨(†±àL QJ=) Ò<ÓMoÃ6Ûɸ\43×ò¯/O_½8âå2òPÙ´Rÿ…èámÈYgG7¢ùY+ž—%ʾ‰¬nµü¾|ÿOûcг<C390>¤G ì-—µ¡Îï7ŒÞȸyØÅ˜¹ŽÌSŸ}07Ý™ã¿îõfjÌ-­“RH…|õǦ  Qr£ûÝ̲a÷<>ãc9vóß³™¢ÌbCø2<C3B8>jù[ —¤É?`cÿ[¬ª+&s-ËùëËøÅä®ù›{öÿ¯.6ÛªbW³´ìÖj y)BÂ-M6Âs#ëU[W®Ë<C2AE>nÌ8Ù¥véß¹HÌ è7ÓùÏz­Î+9p2pfñ齌ûåYZÂW6°œPpÐÞ<C390>½äkòRoq²‡Ž?ü¤Ñ¡rÿ?qiSýô‡??‘Ë¿Ÿ~zúÛßõu~¶pØ·u=mŒxÞìl*”QÕÓa<C393>yb-¬Õ<C2AC>ô!/áÔŸ #U±±DDvÈž-Bú°)òA<C3B2><41>»öÌÈ ôPô*|žND,<2C>íœãè~ðì}åá&kÄ;£Œ— Ýu­½'àÖf®ÅÕÍo¥Ö²°¡L„ò}n[n%k[ÿÊñ_—¾h0öæD÷¡_g»Öö 'Äh²Ö ¥ÆŒ #‚©åÖÔÏ­ÍÆÕW·™…ݳ®ô0u[,(rm”ß¿š£"WŒ²—ˆ”1»—f÷àÌ.{_š"Õì¸ j¤Xûö»ÄIõã“~LÞëÏol•»øÇìç¡Ánnºý<ÓÒËEº9·Çgœ‡YÒi¦1»<þ¶^­Ïnùa¯v<C2AF>tÛtCœ”nï ½¯t{é[¤ª+v™ÊfgSñŒª®xãéö⌟ âÉŒ)û¥Û7…ÿA<C3BF><41>»úÞ\_Æ/윲@E¯ÂçéDÄòØÎ9ŽîÏÞW¹tÛ7â<37>ÑÆ„K<E2809E>õ¯5²wN·= K·ë`C™åûÜÓíBü×¥/Œ™ø†Òí_cÛÜ‹*0R6ã´÷LÜ-cüÑ¥³ú5Á.ó¤Ë}'³ùV3YŽÓ 'Ó¨`. 56Ì©æX{KÚf¯aÚ«<C39A>a<EFBFBD> Ó<>aaöÍ— Éd® Ê6ù€|ßó6>â®Jé;xÕšR?,¨i?=îvcÿqgÊêÑŤ-’¤+””ƒö8TØ<¦ùaaÿy.¶ÖON¸cÖ÷ô«»Ý(-œgr®ÚÌPXO,œ rƒÒSŒ÷#aļII†å º0ñkLTÚ!,å\+»™Šywx©0tHzo@/ ^‘û›»<E28098>•Oó|íãÑÌÛ„h#Eg÷~L^1Sƒ$„ðl­ìüÇõ2Ån>tôŠ>ì´®RÂ<52><·Ú˜äÁ¾÷i¦j|änÑ!óT`<60>ÿâl9ÙLÄ{oµá¢ue4ç¥q†°0;9—h;](_þk¯ajG“NZp:Í·0K±LÊñSò3I[B…M<êå—ªGÍ<E28099>örIšüc=eżpã&Œ5R9Ö)Ëö*î•gg¿[>}9¼àœæû<C3A6>·8øžÅž­kªoª®˜ü°ov6ʨêŠé0À<±ÖjHzP—pi€Ï£®ßql*<11>ÄæK!œ=ãø¦Ð?®w¾×yZqlçG÷Bç9w§3cÂ;¾ˆê[kT»÷9ëAË­_-k^9öïùç£6]öº–!7; tÍgå|ËM—
dÀ”HœüâÂ>ËÛ¯|×'ô¥<C3B4>QI˜½\&ÿXdOEY1/$FOÌ ¥ÅßAíV¸¼´ #,ø@…Âr¼60¦ËkžpQ¡¡ý ÈØ Ï/„ °çw8´Sîà-a¾ç¼ºoëšê{¤ª+&|<EFBFBD>M…2ªºb: sO¬…½.¼&|ÚÙg6`L„%9*܆~>¾=ÓËMXx§Xîà09L+2Yg1<67>Da•VL'Û9'h½p,{ÝÏJÓ&ç7€>ùC}> ­ºæâõ¬g]*È<>¹À9##YÜœ•^øÕÒ„®£ ÞQš<C5A1>Ó:'°n\<¦Z}µ5²7ŠÒ[ɺ`S±Üò¯¨õ–fRf_½^™¬ë‹û°ab°pfÿ;¹$Mþ±6Gc<+æo}S.Õóļ0ù]Ù¥ˆjɾ\ÖOVè±<C3A8>מ4^§) Ù„‹ò í‡xÎ^x~><04>=¿Ã¡<C383>ro ó]3»][×Tß#U]1ùàÛìl*”QÕÓq˜ob-ìÕ<C3AC>ôá5áÓÈ>“‰Ød",ÉQáìåãÛ3½Ü„…áwKJ#ßFªºb7½EiAUWL'Û9'hÝp,zÝGdÆdâü:è#<23>¯ 84i<34>„œûâAs‰õ¬g]ÚÀ¹O¿»s<C2BB>“F†§jBV{TºáWIúrŒ&xGiFvNëœÀºqñ˜Ê$hõõV&ŸK•‰@öSœo¾2qëË(\ª`>¾ƒ”®|vrIšücí4¢HVÌ Ûú6
—êyb^˜ü®ø)âܳP.IÛg¾Õ ¤TŒíGyè¢|CÁÏØ Ïχ °çw8´Sîà-a¾ëB·këšê{¤ª+&|<>M…2ªºb:óM¬…½>¼&|ÚÙÝÂY KrT8{ùøöL/7aáAøÝÒÈ·ª®ØMoaÚ_RÕÓ‰C<E280B0>ÆvÎ Z7™1™8¿úä+MZ'!ç¾¥xÐ\b=ëY—6pîÒïþ\à¤á©š<C2A9>Õ•nøUÒ„¾£ ÞQš<C5A1>Ó:'°n\<¨2Ù·új+Âò• <1F> 9o¾2që q˜”JìÊZ_ʽž¶£ì<C2A3>RÒ/·ÕŒ¸-g_Ì “á~BH<>\9ša)ínRhf?œ3ÖÂó3«¡<C2AB>r׎„óž ™ßÒ5îo (‹Ó¯íüE<C3BC>в8uƒwJÁ,:»{·ZÀç/wyw'±íl¥#Ö6!ŒëC¸%<25>#U]±<C2B1>´¼¤ª+¦`-Ãóo PÌÊö±6˜8¿åºK¨;{y2î[
ÍÅkLÏzá<7A>o—þö¯Îç ßW5¡©=*½<>+/Ý]«~  £¥?;_uNNݘxPU°oõÕVó÷ù³Ÿ¤¢tTo¿*p+ˆTnJó«TrWTîå4ùÇÚ¬JÒ¬˜Ö5Ì\ÚÆË,æ…ÉԹž…rIZg Žõ<AÌÇýT-ôP¾<50> WK  0çw7´SîÞ-A¾kƶkëšê{¤ª+&z<7A>M…2ªºb:òM¬…½.´&\Zì.é+%9$œ¹|t{¦<E28093>ð ônihäÚHUW즶 ±/ªêŠéÄ<C3A9>Àb;ç­<17>E§û€L[Lœ^‡|ñ‡&¬“psßò:h.±õ¬I4ý¤û@¸ðTMÀj<C380>J/ú*BWvÑî(ÁÈÎg<C38E>“W7*S­¾ÚŠÄþØRþTTùû£o»"YÖ9K`Di8 ²Ü–ÛË%iò<69>EöT™ó‚·´¹o“r¯½euŸ.#ØóÂ2S0óû˜<1C>Œ1¥4¢%Ox&ÛÈ~Xg¬gÜÌ^G¯í=»%¬÷\Õöm]SýŽTuÅäƒm³³©PFUWL‡a퉵<E280B0>WCÒÔ„OÀúŒ<07>c‰¨¤Fƒ3—<33>nÏdr„Þ-ÿ =jj²?™E©}VS“§ñ¯b;çĪ„%gû0LŒÏnÀyì%ð‡:|Zî[cï[‹Ö­æhÃã>©îZãož¦C­!è„YeíïÉš0f¹¹ªgfêþƒêŠ]£¯·¬Lûs¦¹ú #{üOÌÞ¡¾XV÷1(&˜³"xtàßÉ%iò<69>µ™— Y1/l sùÛ&å^O^/üœ<C3BC>-ØóÂ:OŒrL²ß;ˆ<“k$HÙ2Ö³ónÌy½¶÷ì°Þ5CÛµuMõ;RÕ¶ÍΦBU]1‡õ&ÖB^ IP>­ƒÕß…+G%5œ¹|t{&“<E2809C>ð ônifèÙPS“ýÉ,Làóšš<<3C>ˆÛ9'VÝ ,8Û‡aÆ`|vç°×Àš¦N@Ë} è}kñºÕ¼­xÜåÔ}kü ƒ`Ó4`¨5½0+¯ý]yC †ÃÌ!7WõÌL}ÑL}±oôõÖ¼xÛ1ÌÞþmeá@îÇrÎ)äJÓ½\&ÿXŸ+0%æ…máBÒ%nž˜&¿+~‡ä2|}¹$-ňЀ‰ö¶¸è+ÄãH•J¸¨ÐÐ~dgì…çBÌ~‡C;åÞæGÐ "Áqé{¤ª+&|<>M…2ªºb: sO¬…½.¼&|ÚÙg=ØPl2䍨3&âÛ3½Ü„…áwK8#ßFªºb7½…ù|IUWL'Û9'h½p,{ÝGdÚdâüÐG _@p¨Ïg!ç¾¥uÐ\¼žõ¬K8w9w.pÎÈðUMÈj<C388>J/üjiBWŽÑï(ÍÈÎi<C38E>X7.T˜ì[}½•‰x—¨ŠiÜ”2N·åy?0<>ÇM<02>ð¸<01>ð¸<01>Û
¶µàqË-wÑ ÌŒ¼¬ó€ÇÍ~Ó;Jà¯ñLªœ¦žFM=P¸áH
·Ãa
·€·êÌ<>Â-º6 p
·LЀÂí8r€ÂíðÈ
7 p{åñË<C3B1>/ú¡ç_¢é?1Ÿµ"„OÚAZàI+„xÒ€' xÒ€' xÒ€' xÒGxÒ€'í`úO>êǧRú?ʨlHÿ<48>Œ,’€Œ ÈÈÜ‚dd@FddíCÈÈ€Œ ÈÈ€Œ ÈÈîšþO®ȧÿŒK16¤ÿÀøŒ_Àø…ƒÀøŒ_ÀøŒ_§ ¿#¿€ñë•¥Þv×½”zS¡ûÁê©7ÐjÅÐj ´ZÐj­Ðj­Ðj­Ðj­Ö=ÓþÏn×=›öÜ´áÔUÃPW¥Â
ÔU@]ÔU@]µÚê* ®ê* ®êªc_•ÅîQøº,C„6ìÚ5ÔÆ`ÔP©°5PC5PC­v€
¨¡€*úè PCe£ÿ§†ŠòwRþ¾+$Bbù!và`‘ LÀÁLaqLÀÁtÓP&à`&à`¦{—öû®…*<C2BC>ORT a²ö¨ŽüÕ‘ª# :ª# :ª£lk3@u”[î¢A˜yYçÕÍöäË…ëÚ€a¦sóïfÞ#Duê'çÏD“é Gç#Ußþ£Xþ#à?r_Êþ#à?þ£ö¡üGÀüGÀüG÷å?z¹0]#Ìߺ4_Ð$/Úà FЉU`, °(´3R,30#¹ä˜‘€ ˜‘Ú‡0#30#5E˜‘€é03ÒË…ë_‰éRP5 >—äSº,àƒR1ÑPcR$c0&¹…“€1 “Ú‡0&c0&c0&Ý—1I—t`<>Ü•s­<73>¾E0Úqo¨€> è“€> <06>> è“€> è“NAÐ'F>Ð'}Ò+ËÃÓÛïBGÜ<>qkR,kR!ÈÀš<14>Åk°&k°&k°&k°&Ý•5éå2¢AH÷™|óYœ<59><å>Œcй6”@§´þê?Ð)¥Â
tJ@§tJ@§´Ú:% S:% Sè”Z¿p«±«í˜n%àVº·ÒkÜJY°·Rz·œ¸•Ç
¸•€[ ¸•€[©£o™[©éK²:øHR%[hR<68>f)€f hfÐ,ÍÒQünIÐ,ÍÐ,ÍÒ>*@³4K7Ñ,E¿Ÿ“ý¢¬®FIMҮ渗¨î-Æký“Wá4&;«C¤ˆŽb®öñûG© j[ÿÈ«ë[ýbÖj9¶­<C2B6>Y؇í£rW«ÛÆ¥Ÿ?<3F>Ë÷ÿÜ <0A>Þîàµ;½\ž9J}€Œ´öäU¹õ¤á/ÉeØ3ì¶t_»uHÎD¿<44>ÇÁþæPLõCY-¦\`µÙp<C399>Ï3%þœìµ”î~¿áÜ>…Œ<9EâÔJAå}Z[û‡"¨{0&e€eÆ8“ŒÙ…Уaó“Â4´ ИúèöÀÌ]e4î){žd˜´OÆop±atL"Eþjýó¹|²HAÝHai¢ŸqЧSÑÎß7¤j†2;úâXP:õ-£Aw{Ž~$Üi8½W=ÒA"AYv¯ÚóÛˆµ‡ÌÕ÷<C3B7>¡Adú˜Â‡0¿‰ôëŠ l<e,o4d9|œïo$üǨ!<21>-ÜÐ𨫢÷\´Œzò8»h)× ®¶oÎ…ÂjùúºÊ„ãaï¥#ã{é/ï¥#òuuä¤ä«Ö<C396>k{[¾š×E[ï£y<C2A3>µ´éÞlšÄkY'9&¯¸r5 ò¶³:kÔYâÜqŒš¯"êšÛˆs<0E>¿ÌG=ëœZØDÒî­?ÙÆœ¸ê‡reë¸9Ô¾·ÈŸÝkáäÌ®ª`¥£¢²ÅÛ\VVퟭ7¯ObþŒ¨}Úþ™ŒBŒóÁÿ†<C3BF>×¾åŸàô1»ï¼©¿ÅË­ƒµžeFºÚFz¸=`
xœí]ݎ㸱¾ï§ð ´Â?°èdO=6;@.\Ìhfr0}‚Í¢DŠÿ”e¯»·¶×cWY*ŠUÉ*Êö‡/Hý=cõ<63>dx<64>Bx¼Ì¯O?Û?lÀ:êÿÕ?ª¿ä?{ëèBbßu:Œ¾h÷ý—ÿÿßùË_þôûËüË2Gü2ÿßÓïþô ¾üã<75>Û™‰üýϪ÷Ö<65>äÉåÃ×§¿}‡RzÄÔƒª÷…z¨—ø³úgTÏxyFüå"ù0a†8½è“ÈË…Ða"£<>Ø(Ä‹º
9Œ#ãR<18> ¿_>ü÷Ó}P—º^Œ<> ¢$㲟<C2B2>ÛÜŸï#,Ä€&J©\Ž~“>$§ûP¢aDxiéÈ,9ü ûðË-|ÈbLù0ð¶Qüp^“½£™Èħ@ÖNªéI¢ËëÓÈÑ@¤rç¤åo<C3A5>\fÿX…uåªo±ª®˜õµ¬ço/ã³½æoöÙÿw~úŸ§¿^œ“ƒ “dSAF6©á£ÆÃ0<C383>db Î¥T“Ò õ3uz¶ÅþHÆõ¢™¾}yúêů—‡ŠÓJõ¢‡·!gíˆ^åg¥x^—(ó&2ºÍòøòý?mì<6D>AÏ4j G<C2A0>h%°·^C<E28093>ßo"/½qs·ÑsY¦>ó`vºÓÇÝëõÔ˜[Z<>¾óÕDÉŽîw3ˆÝcô<77>åØ-ÏzŠÒ áë4ªäo<C3A4>\fÿXµýo±ª®˜õµ¬ço/ã³½æoöÙÿ¿ºØ¸UŬ&ziÙ­5Ô,@ËR„„]šL„—F"¶«6®ÜÕ$Yp²KÍÒï¹HÌ ³è7 ÓåÏxmÎ+9p2pfð齌ûåYZÃW6°žPpÐÞ<C390>¹ä×ä¥^ãdøI¡C%äþâÒ¦úé~"—?ýôô·¿«ëülà°oëõ´ØÙq*”QÕóa<C3B3>yb-¬Õ<C2AC>ô!/áÔŸ #<03>bc‰ˆì<CB86>½X*„ônSä<53> »4÷Ú3#¯ÐCÑ«ðy>±<¶sŽ£ûÁ³÷•‡¬ïŒ0F \ƒtÓµö€Ûšy-®Fhy+µ–µ€ e"”ïs{Ør+YÛúWŽÿ¶ôEƒ±7'º] ý:Û¶¶O8Ù F<>µ)5fl¹lºSnMýÜZo\}µYØ>«JS»±Å²ù·Ž"WFùí«9*rÅ({¹L2föÒÌœÞe³ïK]¤ê·a)V¾ý.qÒGõø¤³÷ú³ó<C2B3>©rWÿèý<4˜ÍM»Ÿ§[z¹ÈA5g÷ø´ó0K:MW"z—ÇßÖK­»õÙ-?ìÕ.<2E>nënˆ“Òí½¡÷•n¯}TuÅ.SqvœŠgTuÅO·Wg”@øLOfL‰ˆÜ.ݾ*üwìÒÜ«ïÍíeüÂÌ)+ôPô*|žOD,<2C>íœãè~ðì}•K·}#Þ `Œ@¸éPÿZ#{ãtÛÚ±t»6”‰P¾Ïía»:Ý.Ä[ú¢Á˜<19>o(Ýþ5v°õ½¨q £Édœæž‰½eŒ?ÚtV½&Øfžt½<74>be¶Üj&ëq*ád
Ì&¡Ú†>UknIì5L{•1¬ra:0,ô¾ùz!a#™ÌU<C38C>a2MÞ!ß÷|‡µ<E280A1>¸­RDúNÆiK©ïÔ´Ÿîw‡Û1Íÿ¸3eJ„éÞŤ)’¤-”& íq)¨°~ÌËÃÀþóRlmŸœ°Çlï­èW{»Q83ÎäRµé¡°<C2A1>X0¼#8.5ä†IM1“¸ #æMJ2,Ñ…‰_c¢RŒ`)—ZÙÎTÌ»ÃKí„¡B@Ò{jiðŠÜß̈Üí¬|Zæk<1F>zÞ&D<>MtqïÇä³i<C2B3>~‡­<C2AD>ÿ¸Z¦ØÀõ‡Ž¾Cч<C391>¶UJØgW<1B><˜÷>-Ãt¹]tÈ2˜ã¿X[VÖÓñÞÛlØ…h[õyiœá‰<58>œK´<4B>.”¯Kÿ¿•×05£I%­+8­æ[˜¥:åø)ù™$×<C397>Ca<06>jù¥„ªdò£½\fÿXdNEY1/\¹ #F…TŽUÊâ^ŽòììwkÓ§¯‡œÓ|¿óß²¸Ø·õšê[¤ª+f?ìÎŽS¡Œª®˜Ìka­†¤u —ø<ªúǦÑHl¾ÂÙ3ޝ
ý<EFBFBD>àzã{<7B>ç¡ÇvÎqt/tžsw:3&¼ãp С¾µFõ·{Ÿ³´ÜúÕ²æ•cÿžïqÞkÓe¿q j2p½B·|V.·ìð×t©@L‰ÄÉ/.ì³¼ýÊ÷úD<C3BA>º2NÒf/—¤Ù?™SQVÌ ‰¤ÑóBiñ·PG»./­Ã >P1a9^Så5O¸¨ÐÐ~Pdì…çBØó;Ú)wðš0ßr^Ý·õšê{¤ª+f|ÎŽS¡Œª®˜ÃÜka¯†¤ ¯ Ÿ¶@ö™ xŠM&Â’vC?ßžéå*,Ü ¿.Ç);8LÓŠLÖYL Q˜ó‰CÇvÎ Z/Ë^÷³Ò´ÉÄù  <>àPŸÏBÎm«® ¹x=ëY—
rg.pÎÈH7çD¥~µ4¡+Çhƒw”fdç´Î ¬÷©V¶&@æFQz+Yl“Dì÷<>ü+j½%€Ù€&½¯^¯L¶õÅ~Xˆ0183ÿ<E280B9>\fÿX“£1žó·¾M6Õóļ0û]Ù¥ˆÓ:}¹$mŸ¬Pc3®<©½N šd.Ê7´â9{áùùöü‡vʼ&Ì7Íìvm½¦ú©êŠÙŸ³ãT(£ª+æã0wb-ìÕ<C3AC>ôá5áÓÈ>“‰Ød",ÉQaíåãÛ3½\……;á×%¥o#U]±›Þ¢´¿ ª+懂ˆíœ´n8½î#2c2q~ôÈ7š´NBÎmKñ ¹ÄzÖ³.9pîÓïî\à¤á©š<C2A9>Õ•nøUÒ„¾£ ÞQš<C5A1>Ó:'°n\ܧ2 Z}ÜÊäs©2È|ŠóÍW&v}…MôÇwФ*G Ÿ<>\fÿX3<58>L$+æ·¾<C2B7>¦zž˜f¿+~Џô,”KûÌ÷4)'Æö£<tQ¾¡`ˆgì…ççCØó;Ú)wðš0ßt¡Ûµõšê{¤ª+f|ÎŽS¡Œª®˜<C2AE>Ã܉µ°WC҇ׄO »[8aIŽ
k/ßžéå*,Ü ¿.)<29>|©êŠÝô¦ý%U]1Ÿ8hl眠uñèu“‰óë <C3AB><>àФurn[ŠÍ%Ö³žuÉ<75>s—~÷ç'<27> OÕ„¬ö¨tï&ôåMðŽÒŒìœÖ9<C396>uãâN•ɾՇ­LËW&|@rÒ_Èyó•‰]_ˆÝĤTª`[ÖúRîõìŽ2?þIERH¿t«±[ξ˜fÿÂý„<C3BD>¬[¶;¹$­sÕ7Â(š”»ý1½wH¡™ýpÎX Ïϸ;˜ü®†6Ê];Î[.d~K¯qEYœ¼Üù«%eqîï&”Ytvö"oµ€Ï_îòîNbÛÚJG¬mB8×» Ð%<25>#U]±<C2B1>´¼¤ª+æ`-Ãó¯ PÌÊö±6˜8¿åºk¨;{y2n[
ÍÅkLÏzá<7A>o—þö¯Îç ßW5¡©=*½<>+/Ý]«~  £¥?;_uNNݘ¸SU°oõa«åûüÙORQ:No¿*°+ˆœì"•úW7¨ä¶¨ÜË%iö<69>5Y•¤Y1/lk˜¾$äÆË"æ…ÙÔÙž…rIÚf ŽÕ<AôÇýT-ôP¾<50> WK  0çw7´SîÞ5A¾iƶkë5Õ÷HUWÌ>ôœ§BU]1¹ka¯†¤ ­ —Ö»KúŠAI k.Ýž©å*$Ü ½. <0A>\©êŠÝÔ$öEU]1Ÿ8Xl眠õ¢±èt<1F>i‰Óë<C393><C3AB> ¾AàЄunn[^Í%Ö²ž5ÉAÓOºd'<27> ¬ö¨ô¢¯’!teMàŽŒì|Ö9yu£â>IÐêÃV$æÇ–òŸ ¢’,ßɺ¶,Y#“‚³ ëm¹½\fÿXdN•Y1/xKMñœ”{í-«û´pÁž˜Ö™éßÇähd\<5C>©I!Zò„g²<67>ì‡uÆZpvÁíÁ,áuôµ½gׄõ«Ú¾­×T¿#U]1û`svœ
eTuÅ|ÖžX y5$]@Mø´¬Ïx˜ˆJj4XsùèöL&W!áNèuùgèÙPS“ýÉ,Jí³šš<Ÿˆÿ)¶sN¬zAXr¶ôÁøìœGÀ^¨Ãg å¶5ö¾µhÝj^€÷Iu×ý ð4 j A'Ì*kOÞЄá0sÈÍU=3S_ôïTWì}ܲb|Ñ?0MÌÏ™æê 6Œìþ?1{ƒúb]AìÇ ˜`
ÌÁ£ÿN.I³¬É¼ÍŠyÁ-`ÌæoNʽž½^ø9[°'æ…mžä˜$d¿wy&×H<C397>²e¬gçݘó:úÚÞ³kÂzÓ m×Ökªßª®˜}°9;N…2ªºb>k'ÖB^ IP>­ƒÕß…+G%5¬¹|t{&“«<E2809C>p'ôº43ôl¨©Éþd&ðyMMžOÄ?‹íœ«nœíÃ0c0>»ŽóØ[àMS' å¶ô¾µxÝj^€6<îrê¾5þ„Aà4 j A/ÌÊkWÞÐá0sÈÍU=3S_ôïS_ì}Üú‚o[0†ÙÛ¿m±.ÈþXîÊ9…liº—KÒìës¦Ä¼à.$mâæ‰yaö»â'|H®Ã×—KÒ:QŒ ˜(oºB<Žtš.*4´Ù{áù…3…ßáÐN¹ƒ×„ùˆGp\û©êŠÙŸ³ãT(£ª+æÃ0÷ÄZØ«!éÂk§-<2D>}Vƒ Å&aIŽŠ=Sa"¾=ÓËUX¸~]Âù6RÕ»é-ÌçKªºb>q(ðØÎ9Aë…cÙë>"Ó&ç7€>ù
C}> 9·-­ƒæâõ¬g]òÀ¹Ë¹ûs<C3BB>sF†¯jBV{TzáWKºrŒ6xGiFvNëœÀºqq§ÂdßêãV&â]~ *¦q&ítàq[Ÿ÷xÜ$ð¸<01>ð¸<01>ð¸m`Ûš·Ür ÂÌÈË:xÜÌ7<C38C>±¥þê(ˆRå4õ4¸t4õ@á†#Þªc0
·èڀ (Ü2A
·ãÈ
·Ã#(Ü€Âí1Êâ—_ÔCÍ¿dJ¦ÿDÖŠ<ii9€'­fàIž4àIž4àIž4àI;àIž´ƒé?ù¨ŸJéÿH(£²!ý2²H22 #³ <02><19>µ #22 #22 #»iú?Û Ÿþ3.ÅØ<C385>þã0~ã
ã0~ã0~<7E> `ü:Œ|`üƯK½Í®{)õ¦BõƒÕSo ÕŠ% Õ*6! ÕZ- ÕZ- ÕZ- ÕZ­[¦ýŸí®{6í'¸¨«ÃPW¥Â
ÔU@]ÔU@]µÙê* ®ê* ®êªc_•ÅöQøº,C„6ìÚ5”c0j¨TX<54>
¨¡€
¨¡6;@ ÔP@ ¨¡²ÑÿÍSCEù;)ß•!±þ;p0áH&à`¦°¸&à`ºj(p0p0Ó­Kó}×Âw^ÅÀõ'©HªˆÉŽ0Ùú TGþ<>êHÕPÕPÕѶ­ :Ê-wÑ ÌŒ¼¬ó€êÈd{òåÂUmÀ0S¹ùw ï¢*õ“–óÈç?¢Ét<C389>£<EFBFBD>sÑTßþ£Xþ#à?²_Êþ#à?þ£ö¡üGÀüGÀüG·å?z¹0U#,ߺÔ_Ð$/Êà0<C3A0>£À$Y`1P$Ç–hf¤Xf$`F²É0#30#µ`Ff$`FjŠ
0#3Òaf¤— W9¾$Óµ0 Ó€øR<16>Oé²€Ó$ e0&E0&c](€1 “€1©}(c0&c0&cÒm“TY@&øÈmY°Ô
é[£ù÷†Zè“€> è“P`è“€> è“€>éd}Òaä}Ð'=XžÞ~*ÚŒà†ŒX“b X“
AÖ¤€,X“€5 X“€5 X“€5 X“€5馬I/— BÚÏäëÏâìä9÷a<>´¡:¥íWÿ<57>N)V S:% S:¥ÍÐ)<01>Ð)<01>Ð)]E§Ôú…[…]eG_$p+·Ò¸•öøÛ’àVʸ•ºÐër:àV:+àVn%àVŒ¾en¥¦/ɪà#I'ÙB“
4K4K@³´¨€f hŽâ×%y@³4K@³4Kû¨ÍÐ,]E³ý~Nö²ª%A4I»šã^¢ª·oýõ{L hLvV…h"*йÚÇï¥:¨mý#×·úÅlÔ²l[ŽY˜‡éãd¯Vµ?Œk? —ïÿ¹½ÝÁ[wz¹<s”úiíÉC¹õ¤á/ɇI³g˜lè2¾<77>\84ˆz<03>ƒ-üõ¡˜ªÇd´˜r<CB9C>'gÃ>>/”øs²×ø=^Ê£väÉY §&g²*oKŠBØÖ?lƒ@íƒÙøM:&¸Œ3ɘ™³QmêXS4<53>žº¡5èÚÔGû0fn䢡p×ó$ä}þ{ƒó;£c)òWëŸOŸ“E
êF
Ks댃:<3A>Š&(pþ¾¡ §f(Lz “Ê6ËhPÝ^¢ß {No<0F>t<EFBFBD>HPÝöü6bå!}uäýbcdh™>¦ð!ôÏÝr@ÁºbOë<1B> Y—[
ÿ1ªyZ ÷|ªBä=× £š<ήÊ©¹­>ÍKn¾Yþ<59>>Vf~¼#ì½td|/áï¥#â½tD>VGÞaN¾*ý×¥œ6ó§e]4%6ZÖX³/š®¢õœþ ºucòÀ•«&m7<6D>UY£ÊúçÖSûOÕÜZ\rüe9êYåÔÂ$f;Cû1È6ÄU=&[¶ŽÎ¡æ½Uþl_ +g62('*[¼ÍeeÕþÙxóõI,Ë4OîŸY+ĸ¼ü¯ pÍ[þ Vê.ûè.^æh¬í,=Ò'7ÒÃmÙÿ§os2
endstream
endobj
77 0 obj
@@ -1396,7 +1405,7 @@ trailer
/Size 87
/Root 3 0 R
/Info 52 0 R
/ID [<f859d5aa137a1b00be187261c0c2b77d> <f859d5aa137a1b00be187261c0c2b77d>]
/ID [<7800bd1e70bdb9114e48fc6d480ec696> <7800bd1e70bdb9114e48fc6d480ec696>]
>>
startxref
101711

View File

@@ -66,8 +66,6 @@ if (process.env.IS_INSTANCE !== 'true') {
name: 'Albert AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
});
await mockAIResponse(page);
@@ -133,8 +131,6 @@ if (process.env.IS_INSTANCE !== 'true') {
name: 'Albert AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
});
await mockAIResponse(page);
@@ -170,11 +166,6 @@ if (process.env.IS_INSTANCE !== 'true') {
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
});
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
@@ -238,11 +229,6 @@ if (process.env.IS_INSTANCE !== 'true') {
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
});
await mockedDocument(page, {
accesses: [
{
@@ -317,11 +303,6 @@ if (process.env.IS_INSTANCE !== 'true') {
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
});
await mockedDocument(page, {
accesses: [
{

View File

@@ -65,6 +65,19 @@ test.describe('Doc Editor', () => {
toolbar.locator('button[data-test="createLink"]'),
).toBeVisible();
/**
* Because of how Posthog is loaded and how auth session are
* saved, this assertion is not reliable on test instances
* We will dedicate a testcase to check the AI features
* on test instances with a specific setup
*/
if (process.env.IS_INSTANCE !== 'true') {
// eslint-disable-next-line playwright/no-conditional-expect
await expect(
toolbar.getByRole('button', { name: 'Ask AI' }),
).toBeVisible();
}
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeVisible();
@@ -96,6 +109,7 @@ test.describe('Doc Editor', () => {
await image.click();
await expect(toolbar.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeHidden();
@@ -688,23 +702,25 @@ test.describe('Doc Editor', () => {
test('it checks interlink feature', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const { name: docChild1 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-1',
);
await verifyDocName(page, docChild1);
const { name: docChild2 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-2',
);
await verifyDocName(page, docChild2);
const treeRow = await getTreeRow(page, docChild2);
// To let the time for the emoji-picker to load
await page.waitForTimeout(500);
await treeRow.locator('.--docs--doc-icon').click();
await page.getByRole('button', { name: '😀' }).first().click();

View File

@@ -104,9 +104,6 @@ test.describe('Doc Header', () => {
browserName,
1,
);
await writeInEditor({ page, text: 'Hello Content' });
await page.getByRole('button', { name: 'Share' }).click();
await updateShareLink(page, 'Public', 'Editing');
@@ -119,9 +116,7 @@ test.describe('Doc Header', () => {
docTitle,
});
await expect(otherPage.getByText('Hello Content')).toBeVisible();
// Wait for other page to broadcast sync
// Wait for other page to sync
await page.waitForTimeout(1000);
await page.keyboard.press('Escape');
@@ -129,8 +124,9 @@ test.describe('Doc Header', () => {
await expect(elTitle).toBeVisible();
await elTitle.fill('Hello World');
await elTitle.blur();
await verifyDocName(page, 'Hello World');
// Wait for other page to broadcast sync
// Wait for other page to sync
await page.waitForTimeout(1000);
// Check other user page
@@ -148,36 +144,6 @@ test.describe('Doc Header', () => {
await cleanup();
});
test('it pastes plain text in the title without keeping formatting', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-title-paste', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await docTitle.click();
await page.keyboard.press('Control+a');
await page.evaluate(() => {
const el = document.querySelector('[aria-label="Document title"]');
if (!el) {
return;
}
const dt = new DataTransfer();
dt.setData('text/plain', 'Pasted plain text');
dt.setData('text/html', '<b><em>Pasted plain text</em></b>');
el.dispatchEvent(
new ClipboardEvent('paste', { clipboardData: dt, bubbles: true }),
);
});
await docTitle.blur();
await expect(docTitle).toHaveText('Pasted plain text');
// Ensure formatting tags from text/html were not inserted.
await expect(docTitle.locator('b, em, strong, i')).toHaveCount(0);
});
test('it updates the title doc adding a leading emoji', async ({
page,
browserName,
@@ -535,7 +501,7 @@ test.describe('Doc Header', () => {
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
);
await mockedDocument(page, {
const uuid = await mockedDocument(page, {
abilities: {
destroy: false, // Means owner
link_configuration: true,
@@ -556,6 +522,7 @@ test.describe('Doc Header', () => {
name: 'Share',
exact: true,
});
await expect(shareButton).toBeVisible();
await shareButton.click();
await page.getByRole('button', { name: 'Copy link' }).click();
@@ -566,8 +533,8 @@ test.describe('Doc Header', () => {
);
const clipboardContent = await handle.jsonValue();
const url = page.url();
expect(clipboardContent.trim()).toMatch(url);
const origin = await page.evaluate(() => window.location.origin);
expect(clipboardContent.trim()).toMatch(`${origin}/docs/${uuid}/`);
});
test('it pins a document', async ({ page, browserName }) => {

View File

@@ -31,8 +31,6 @@ test.describe('Inherited share accesses', () => {
.getByRole('link')
.click();
await page.getByRole('button', { name: 'close' }).first().click();
await verifyDocName(page, parentTitle);
});

View File

@@ -185,23 +185,23 @@ test.describe('Doc Version', () => {
await page.getByLabel('Restore', { exact: true }).click();
const mainEditor = page.getByLabel('Document editor');
await page.waitForTimeout(500);
await expect(mainEditor.getByText('Hello')).toBeVisible();
await expect(mainEditor.getByText('World')).toBeHidden();
await expect(editor.getByText('Hello')).toBeVisible();
await expect(editor.getByText('World')).toBeHidden();
// The old comment is not restored
await expect(mainEditor.getByText('Hello')).toHaveCSS(
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
// We can add a new comment
await mainEditor.getByText('Hello').selectText();
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Add comment' }).click();
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(mainEditor.getByText('Hello')).toHaveClass('bn-thread-mark');
await expect(editor.getByText('Hello')).toHaveClass('bn-thread-mark');
});
});

View File

@@ -153,8 +153,7 @@ test.describe('Help feature', () => {
theme_customization: {
onboarding: {
enabled: true,
learn_more_url: 'http://localhost:3000/learn-more',
ready_template_url: 'http://localhost:3000/ready-template',
learn_more_url: 'https://example.com/learn-more',
},
},
});
@@ -185,19 +184,18 @@ test.describe('Help feature', () => {
'0',
);
const step3 = page.getByTestId('onboarding-step-3');
await step3.click();
await expect(step3).toHaveAttribute('tabindex', '0');
await expect(
step3.getByRole('link', { name: 'ready-made template' }),
).toHaveAttribute('href', 'http://localhost:3000/ready-template');
await page.getByTestId('onboarding-step-3').click();
await expect(page.getByTestId('onboarding-step-3')).toHaveAttribute(
'tabindex',
'0',
);
const learnMoreLink = page.getByRole('link', {
name: 'Learn more docs features',
});
await expect(learnMoreLink).toHaveAttribute(
'href',
'http://localhost:3000/learn-more',
'https://example.com/learn-more',
);
await learnMoreLink.click();
@@ -243,16 +241,6 @@ test.describe('Help feature', () => {
await expect(
modal.getByRole('button', { name: /Suivant/i }),
).toBeVisible();
await modal
.getByText(/Tirez parti de la bibliothèque de contenu/)
.first()
.click();
await expect(
modal.getByText(/Commencez à partir de/).first(),
).toBeVisible();
await expect(modal.getByRole('link')).toHaveText(
"modèles prêts à l'emploi",
);
});
test('Modal is displayed automatically on first connection', async ({

View File

@@ -131,7 +131,7 @@ test.describe('Language', () => {
await waitForLanguageSwitch(page, TestLanguage.French);
// Check for French 404 response
await check404Response('Non trouvé.');
await check404Response('Pas trouvé.');
});
test('it check translations of the slash menu when changing language', async ({

View File

@@ -13,8 +13,8 @@ export const CONFIG = {
name: 'Docs AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: false,
AI_FEATURE_BLOCKNOTE_ENABLED: false,
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
API_USERS_SEARCH_QUERY_MIN_LENGTH: 3,
CRISP_WEBSITE_ID: null,
@@ -131,64 +131,42 @@ export const createDoc = async (
await openHeaderMenu(page);
}
const responsePromiseCreateDoc = page.waitForResponse(
(response) =>
response.url().includes('/api/v1.0/documents/') &&
response.status() === 201 &&
response.request().method() === 'POST',
);
await page
.getByRole('button', {
name: 'New doc',
})
.click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'networkidle',
});
const responseCreateDoc = await responsePromiseCreateDoc;
expect(responseCreateDoc.ok()).toBeTruthy();
const { id: docId } = (await responseCreateDoc.json()) as { id: string };
const responsePromiseUpdateDoc = page.waitForResponse(
(response) =>
response.url().includes(`/api/v1.0/documents/${docId}`) &&
response.status() === 200 &&
response.request().method() === 'PATCH',
);
const input = page.getByLabel('Document title');
await expect(input).toBeVisible({
timeout: 10000,
});
await expect(input).toHaveText('', {
timeout: 10000,
});
await expect(input).toHaveText('');
await input.fill(randomDocs[i]);
void input.blur();
const responseUpdateDoc = await responsePromiseUpdateDoc;
expect(responseUpdateDoc.ok()).toBeTruthy();
await input.blur();
}
return randomDocs;
};
export const verifyDocName = async (page: Page, docName: string) => {
const card = page.getByLabel(
'It is the card information about the document.',
);
await expect(card).toBeVisible({
await expect(
page.getByLabel('It is the card information about the document.'),
).toBeVisible({
timeout: 10000,
});
await expect(card).toHaveText(new RegExp(docName), {
timeout: 10000,
});
/*replace toHaveText with toContainText to handle cases where emojis or other characters might be added*/
try {
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(docName, {
timeout: 3000,
});
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}
};
export const getGridRow = async (page: Page, title: string) => {
@@ -250,9 +228,11 @@ export const updateDocTitle = async (page: Page, title: string) => {
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('');
await expect(input).toBeVisible();
await input.click();
await input.fill(title, {
force: true,
});
await input.click();
await input.blur();
await verifyDocName(page, title);
};
@@ -268,11 +248,10 @@ export const waitForResponseCreateDoc = (page: Page) => {
export const mockedDocument = async (page: Page, data: object) => {
// document/[ID]/ or document/[ID]/tree/ routes
let uuid: string | undefined;
const uuid = crypto.randomUUID();
await page.route(/.*\/documents\/[^/]+\/(?:$|tree\/.*)/, async (route) => {
const request = route.request();
if (request.method().includes('GET') && !request.url().includes('page=')) {
uuid = request.url().match(/\/documents\/([^/]+)\//)?.[1];
const { abilities, ...doc } = data as unknown as {
abilities?: Record<string, unknown>;
};

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "5.0.0",
"version": "4.8.6",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -15,7 +15,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.59.1",
"@playwright/test": "1.58.2",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-plugin-docs": "*",
@@ -24,7 +24,7 @@
"dependencies": {
"@types/pngjs": "6.0.5",
"convert-stream": "1.0.2",
"dotenv": "17.4.2",
"dotenv": "17.3.1",
"pdf-parse": "2.4.5",
"pixelmatch": "7.1.0",
"pngjs": "7.0.0"

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2020",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -6,7 +6,6 @@ const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
/** @type {import('next').NextConfig} */
const nextConfig = {
allowedDevOrigins: ['docs.127.0.0.1.nip.io'],
output: 'export',
trailingSlash: true,
images: {

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "5.0.0",
"version": "4.8.6",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -23,59 +23,59 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@ai-sdk/openai": "3.0.53",
"@blocknote/code-block": "0.49.0",
"@blocknote/core": "0.49.0",
"@blocknote/mantine": "0.49.0",
"@blocknote/react": "0.49.0",
"@blocknote/xl-ai": "0.49.0",
"@blocknote/xl-docx-exporter": "0.49.0",
"@blocknote/xl-multi-column": "0.49.0",
"@blocknote/xl-odt-exporter": "0.49.0",
"@blocknote/xl-pdf-exporter": "0.49.0",
"@ai-sdk/openai": "3.0.47",
"@blocknote/code-block": "0.47.3",
"@blocknote/core": "0.47.3",
"@blocknote/mantine": "0.47.3",
"@blocknote/react": "0.47.3",
"@blocknote/xl-ai": "0.47.3",
"@blocknote/xl-docx-exporter": "0.47.3",
"@blocknote/xl-multi-column": "0.47.3",
"@blocknote/xl-odt-exporter": "0.47.3",
"@blocknote/xl-pdf-exporter": "0.47.3",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.42",
"@fontsource-variable/material-symbols-outlined": "5.2.38",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/cunningham-react": "4.3.0",
"@gouvfr-lasuite/cunningham-react": "4.2.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.20.1",
"@gouvfr-lasuite/ui-kit": "0.19.10",
"@hocuspocus/provider": "3.4.4",
"@mantine/core": "9.0.2",
"@mantine/hooks": "9.0.2",
"@react-aria/live-announcer": "3.5.0",
"@mantine/core": "8.3.18",
"@mantine/hooks": "8.3.18",
"@react-aria/live-announcer": "3.4.4",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.49.0",
"@tanstack/react-query": "5.99.2",
"@sentry/nextjs": "10.45.0",
"@tanstack/react-query": "5.95.0",
"@tiptap/extensions": "*",
"ai": "6.0.168",
"ai": "6.0.134",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.1.1",
"crisp-sdk-web": "1.0.27",
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "26.0.6",
"i18next": "25.10.4",
"i18next-browser-languagedetector": "8.2.1",
"idb": "8.0.3",
"lodash": "4.18.1",
"luxon": "3.7.2",
"next": "16.2.4",
"posthog-js": "1.369.4",
"next": "16.2.3",
"posthog-js": "1.363.1",
"react": "*",
"react-aria-components": "1.17.0",
"react-aria-components": "1.16.0",
"react-dom": "*",
"react-dropzone": "15.0.0",
"react-i18next": "17.0.4",
"react-i18next": "16.6.1",
"react-intersection-observer": "10.0.3",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.4.0",
"use-debounce": "10.1.1",
"styled-components": "6.3.12",
"use-debounce": "10.1.0",
"uuid": "14.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
@@ -84,7 +84,7 @@
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.99.2",
"@tanstack/react-query-devtools": "5.95.0",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
@@ -96,18 +96,18 @@
"@types/react-dom": "*",
"@vitejs/plugin-react": "6.0.1",
"cross-env": "10.1.0",
"dotenv": "17.4.2",
"dotenv": "17.3.1",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "29.0.2",
"jsdom": "29.0.1",
"node-fetch": "2.7.0",
"prettier": "3.8.3",
"prettier": "3.8.1",
"stylelint": "16.26.1",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vitest": "4.1.4",
"webpack": "5.106.2",
"vitest": "4.1.0",
"webpack": "5.105.4",
"workbox-webpack-plugin": "7.1.0"
},
"packageManager": "yarn@1.22.22"

View File

@@ -1,13 +1,12 @@
import { ComponentPropsWithRef, Ref, forwardRef } from 'react';
import { Ref, forwardRef } from 'react';
import { css } from 'styled-components';
import { Box, BoxProps } from './Box';
import { Box, BoxType } from './Box';
export type BoxButtonType = BoxProps &
Omit<ComponentPropsWithRef<'button'>, keyof BoxProps | 'ref'> & {
disabled?: boolean;
ref?: Ref<HTMLButtonElement>;
};
export type BoxButtonType = Omit<BoxType, 'ref'> & {
disabled?: boolean;
ref?: Ref<HTMLButtonElement>;
};
/**
* Styleless button that extends the Box component.
@@ -60,7 +59,7 @@ const BoxButton = forwardRef<HTMLButtonElement, BoxButtonType>(
if (disabled) {
return;
}
props.onClick?.(event);
props.onClick?.(event as unknown as React.MouseEvent<HTMLDivElement>);
}}
/>
);

View File

@@ -34,7 +34,9 @@ export const TextStyled = styled(Box)<TextProps>`
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
(props, ref) => {
return <TextStyled ref={ref} as="span" {...props} />;
return (
<TextStyled ref={ref as React.Ref<HTMLDivElement>} as="span" {...props} />
);
},
);

View File

@@ -2,7 +2,7 @@ import {
Button,
ButtonProps,
Modal,
ModalDefaultVariantProps,
ModalProps,
ModalSize,
} from '@gouvfr-lasuite/cunningham-react';
import { ReactNode, useEffect } from 'react';
@@ -20,7 +20,7 @@ export type AlertModalProps = {
title: string;
cancelLabel?: string;
confirmLabel?: string;
} & Partial<ModalDefaultVariantProps>;
} & Partial<ModalProps>;
export const AlertModal = ({
cancelLabel,

View File

@@ -49,7 +49,7 @@ export const SideModal = ({
return (
<>
<SideModalStyle width={width} side={side} $css={$css} />
<Modal {...modalProps} size={ModalSize.FULL} variant="default">
<Modal {...modalProps} size={ModalSize.FULL}>
{children}
</Modal>
</>

View File

@@ -28,7 +28,6 @@ interface ThemeCustomization {
onboarding?: {
enabled: true;
learn_more_url?: string;
ready_template_url?: string;
};
translations?: Resource;
waffle?: WaffleType;

View File

@@ -70,7 +70,6 @@
.c__modal__title {
padding: 0;
display: block;
}
.c__modal__footer {

View File

@@ -361,7 +361,7 @@
--c--globals--font--weights--medium: 500;
--c--globals--font--weights--bold: 600;
--c--globals--font--weights--extrabold: 800;
--c--globals--font--weights--black: 800;
--c--globals--font--weights--black: 900;
--c--globals--font--families--base:
inter variable, roboto flex variable, sans-serif;
--c--globals--font--families--accent:
@@ -849,18 +849,6 @@
--c--components--forms-checkbox--font-size: var(
--c--globals--font--sizes--sm
);
--c--components--forms-input--border-radius: 4px;
--c--components--forms-input--border-radius--hover: 4px;
--c--components--forms-input--border-radius--focus: 4px;
--c--components--forms-select--border-radius: 4px;
--c--components--forms-select--border-radius--hover: 4px;
--c--components--forms-select--border-radius--focus: 4px;
--c--components--forms-textarea--border-radius: 4px;
--c--components--forms-textarea--border-radius--hover: 4px;
--c--components--forms-textarea--border-radius--focus: 4px;
--c--components--forms-datepicker--border-radius: 4px;
--c--components--forms-datepicker--border-radius--hover: 4px;
--c--components--forms-datepicker--border-radius--focus: 4px;
--c--components--badge--font-size: var(--c--globals--font--sizes--xs);
--c--components--badge--border-radius: 12px;
--c--components--badge--padding-inline: var(--c--globals--spacings--xs);
@@ -1743,6 +1731,7 @@
--c--globals--font--sizes--xs-alt: 3rem;
--c--globals--font--weights--thin: 100;
--c--globals--font--weights--extrabold: 800;
--c--globals--font--weights--black: 900;
--c--globals--font--families--accent:
marianne, inter variable, roboto flex variable, sans-serif;
--c--globals--font--families--base:
@@ -2550,18 +2539,6 @@
--c--components--forms-checkbox--font-size: var(
--c--globals--font--sizes--sm
);
--c--components--forms-input--border-radius: 4px;
--c--components--forms-input--border-radius--hover: 4px;
--c--components--forms-input--border-radius--focus: 4px;
--c--components--forms-select--border-radius: 4px;
--c--components--forms-select--border-radius--hover: 4px;
--c--components--forms-select--border-radius--focus: 4px;
--c--components--forms-textarea--border-radius: 4px;
--c--components--forms-textarea--border-radius--hover: 4px;
--c--components--forms-textarea--border-radius--focus: 4px;
--c--components--forms-datepicker--border-radius: 4px;
--c--components--forms-datepicker--border-radius--hover: 4px;
--c--components--forms-datepicker--border-radius--focus: 4px;
--c--components--badge--font-size: var(--c--globals--font--sizes--xs);
--c--components--badge--border-radius: 12px;
--c--components--badge--padding-inline: var(--c--globals--spacings--xs);

View File

@@ -372,7 +372,7 @@ export const tokens = {
medium: 500,
bold: 600,
extrabold: 800,
black: 800,
black: 900,
},
families: {
base: 'Inter Variable, Roboto Flex Variable, sans-serif',
@@ -664,26 +664,6 @@ export const tokens = {
'body--background-color-hover': '#F0F0F3',
},
'forms-checkbox': { 'font-size': '0.875rem' },
'forms-input': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-select': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-textarea': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-datepicker': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
badge: {
'font-size': '0.75rem',
'border-radius': '12px',
@@ -1354,7 +1334,7 @@ export const tokens = {
'sm-alt': '3.5rem',
'xs-alt': '3rem',
},
weights: { thin: 100, extrabold: 800 },
weights: { thin: 100, extrabold: 800, black: 900 },
families: {
accent:
'Marianne, Inter Variable, Roboto Flex Variable, sans-serif',
@@ -1968,26 +1948,6 @@ export const tokens = {
'body--background-color-hover': '#F0F0F3',
},
'forms-checkbox': { 'font-size': '0.875rem' },
'forms-input': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-select': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-textarea': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
'forms-datepicker': {
'border-radius': '4px',
'border-radius--hover': '4px',
'border-radius--focus': '4px',
},
badge: {
'font-size': '0.75rem',
'border-radius': '12px',

View File

@@ -35,7 +35,7 @@ const initialState: ThemeStore = {
colorsTokens: defaultTokens.globals.colors,
componentTokens: defaultTokens.components,
contextualTokens: defaultTokens.contextuals,
currentTokens: tokens.themes[DEFAULT_THEME],
currentTokens: tokens.themes[DEFAULT_THEME] as Partial<Tokens>,
fontSizesTokens: defaultTokens.globals.font.sizes,
setTheme: () => {},
spacingsTokens: defaultTokens.globals.spacings,

View File

@@ -1,13 +1,12 @@
import React, { ComponentPropsWithRef } from 'react';
import React from 'react';
import { Box, BoxProps } from '@/components';
import { Box, BoxType } from '@/components';
type AvatarSvgProps = BoxProps &
Omit<ComponentPropsWithRef<'svg'>, keyof BoxProps> & {
initials: string;
background: string;
fontFamily?: string;
};
type AvatarSvgProps = {
initials: string;
background: string;
fontFamily?: string;
} & BoxType;
export const AvatarSvg: React.FC<AvatarSvgProps> = ({
initials,

View File

@@ -11,15 +11,11 @@ vi.mock('@/stores', () => ({
useResponsiveStore: () => ({ isDesktop: false }),
}));
vi.mock('@/features/skeletons', async () => {
const actual = await vi.importActual<any>('../../../skeletons');
return {
...actual,
useSkeletonStore: () => ({
setIsSkeletonVisible: vi.fn(),
}),
};
});
vi.mock('@/features/skeletons', () => ({
useSkeletonStore: () => ({
setIsSkeletonVisible: vi.fn(),
}),
}));
vi.mock('../../doc-management', async () => {
const actual = await vi.importActual<any>('../../doc-management');

View File

@@ -1,33 +0,0 @@
import { sanitizeColor } from '../utils';
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
describe('sanitizeColor', () => {
it('accepts valid 6-digit hex colors', () => {
expect(sanitizeColor('#1a2b3c')).toBe('#1a2b3c');
expect(sanitizeColor('#AABBCC')).toBe('#AABBCC');
expect(sanitizeColor('#000000')).toBe('#000000');
expect(sanitizeColor('#ffffff')).toBe('#ffffff');
});
it('rejects 3-digit hex colors and returns a valid random hex color', () => {
expect(sanitizeColor('#abc')).toMatch(HEX_COLOR_RE);
});
it('rejects named colors and returns a valid random hex color', () => {
expect(sanitizeColor('red')).toMatch(HEX_COLOR_RE);
expect(sanitizeColor('blue')).toMatch(HEX_COLOR_RE);
});
it('rejects CSS injection attempts and returns a valid random hex color', () => {
expect(sanitizeColor('red; behavior: expression(alert(1))')).toMatch(
HEX_COLOR_RE,
);
expect(sanitizeColor('#fff; color: red')).toMatch(HEX_COLOR_RE);
expect(sanitizeColor('javascript:alert(1)')).toMatch(HEX_COLOR_RE);
});
it('rejects empty string and returns a valid random hex color', () => {
expect(sanitizeColor('')).toMatch(HEX_COLOR_RE);
});
});

View File

@@ -15,6 +15,7 @@ import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
@@ -34,14 +35,14 @@ import {
useUploadStatus,
} from '../hook';
import { useEditorStore } from '../stores';
import { DocsEditorStyle } from '../styles';
import { cssEditor } from '../styles';
import { DocsBlockNoteEditor } from '../types';
import { randomColor, sanitizeColor } from '../utils';
import { randomColor } from '../utils';
import BlockNoteAI from './AI';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { DocsCommentsStyle, useComments } from './comments/';
import { cssComments, useComments } from './comments/';
import {
AccessibleImageBlock,
CalloutBlock,
@@ -152,13 +153,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
*/
renderCursor: (user: { color: string; name: string }) => {
const cursorElement = document.createElement('span');
const safeColor = sanitizeColor(user.color);
cursorElement.classList.add('collaboration-cursor-custom__base');
const caretElement = document.createElement('span');
caretElement.classList.add('collaboration-cursor-custom__caret');
caretElement.setAttribute('spellcheck', `false`);
caretElement.setAttribute('style', `background-color: ${safeColor}`);
caretElement.setAttribute('style', `background-color: ${user.color}`);
if (showCursorLabels === 'always') {
cursorElement.setAttribute('data-active', '');
@@ -170,7 +170,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
labelElement.setAttribute('spellcheck', `false`);
labelElement.setAttribute(
'style',
`background-color: ${safeColor};border: 1px solid ${safeColor};`,
`background-color: ${user.color};border: 1px solid ${user.color};`,
);
labelElement.insertBefore(document.createTextNode(user.name), null);
@@ -260,12 +260,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
}, [setEditor, editor]);
return (
<Box ref={refEditorContainer} $height="100%">
<DocsEditorStyle />
<DocsCommentsStyle
canSeeComment={canSeeComment}
currentUserAvatarUrl={currentUserAvatarUrl}
/>
<Box
ref={refEditorContainer}
$css={css`
${cssEditor};
${cssComments(showComments, currentUserAvatarUrl)}
`}
>
{errorAttachment && (
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
<TextErrors
@@ -349,9 +350,12 @@ export const BlockNoteReader = ({
useHeadings(editor);
return (
<Box>
<DocsEditorStyle />
<DocsCommentsStyle canSeeComment={false} />
<Box
$css={css`
${cssEditor};
${cssComments(false)}
`}
>
<BlockNoteView
className="--docs--main-editor"
editor={editor}

View File

@@ -1,41 +1,37 @@
import clsx from 'clsx';
import { PropsWithChildren, useEffect, useState } from 'react';
import { css } from 'styled-components';
import { useEffect, useState } from 'react';
import { Box } from '@/components';
import { Box, Loading } from '@/components';
import { DocHeader } from '@/docs/doc-header/';
import {
Doc,
LinkReach,
getDocLinkReach,
useCollaboration,
useIsCollaborativeEditable,
useProviderStore,
} from '@/docs/doc-management';
import { TableContent } from '@/docs/doc-table-content/';
import { useAuth } from '@/features/auth/';
import { SkeletonEditorCore, useSkeletonStore } from '@/features/skeletons';
import { useSkeletonFadeOut } from '@/features/skeletons/hooks/useFadeOut';
import { useSkeletonStore } from '@/features/skeletons';
import { useAnalytics } from '@/libs';
import { useResponsiveStore } from '@/stores';
import { useCollaboration } from '../hook/useCollaboration';
import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
const DOCS_EDITOR_CLASS = '--docs--doc-editor';
interface DocEditorContainerProps {
docHeader: React.ReactNode;
docEditor: React.ReactNode;
isDeletedDoc: boolean;
readOnly: boolean;
}
export const DocEditorContainer = ({
children,
docHeader,
docEditor,
isDeletedDoc,
readOnly,
}: PropsWithChildren<DocEditorContainerProps>) => {
}: DocEditorContainerProps) => {
const { isDesktop } = useResponsiveStore();
return (
@@ -43,8 +39,8 @@ export const DocEditorContainer = ({
<Box
$maxWidth="868px"
$width="100%"
$flex="1"
className={DOCS_EDITOR_CLASS}
$height="100%"
className="--docs--doc-editor"
>
<Box
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
@@ -70,7 +66,7 @@ export const DocEditorContainer = ({
})}
$height="100%"
>
{children}
{docEditor}
</Box>
</Box>
</Box>
@@ -86,19 +82,23 @@ interface DocEditorProps {
export const DocEditor = ({ doc }: DocEditorProps) => {
useCollaboration(doc.id);
const { isDesktop } = useResponsiveStore();
const { provider, isReady } = useProviderStore();
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const isDeletedDoc = !!doc.deleted_at;
const readOnly =
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
const { setIsSkeletonVisible } = useSkeletonStore();
const isProviderReady = isReady && provider;
const { trackEvent } = useAnalytics();
const [hasTracked, setHasTracked] = useState(false);
const { authenticated } = useAuth();
const isPublicDoc = getDocLinkReach(doc) === LinkReach.PUBLIC;
const { setIsSkeletonVisible } = useSkeletonStore();
useEffect(() => {
setIsSkeletonVisible(false);
}, [setIsSkeletonVisible, doc.id]);
if (isProviderReady) {
setIsSkeletonVisible(false);
}
}, [isProviderReady, setIsSkeletonVisible]);
/**
* Track doc view event only once per doc change
@@ -124,56 +124,30 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
});
}, [authenticated, hasTracked, isPublicDoc, trackEvent]);
if (!isProviderReady || provider?.configuration.name !== doc.id) {
return <Loading />;
}
return (
<>
{isDesktop && <TableContent selector={`.${DOCS_EDITOR_CLASS}`} />}
{isDesktop && <TableContent />}
<DocEditorContainer
docHeader={<DocHeader doc={doc} />}
docEditor={
readOnly ? (
<BlockNoteReader
initialContent={provider.document.getXmlFragment(
'document-store',
)}
docId={doc.id}
/>
) : (
<BlockNoteEditor doc={doc} provider={provider} />
)
}
isDeletedDoc={isDeletedDoc}
readOnly={readOnly}
>
<DocCoreEditor doc={doc} readOnly={readOnly} />
</DocEditorContainer>
/>
</>
);
};
interface DocCoreEditorProps {
doc: Doc;
readOnly: boolean;
}
export const DocCoreEditor = ({ doc, readOnly }: DocCoreEditorProps) => {
const { provider, isReady } = useProviderStore();
const isProviderReady = isReady && provider;
const showContent = !!(
isProviderReady && provider?.configuration.name === doc.id
);
const { skeletonVisible, isFadingOut } = useSkeletonFadeOut(showContent);
if (
skeletonVisible ||
!isProviderReady ||
provider?.configuration.name !== doc.id
) {
return (
<SkeletonEditorCore
isFadingOut={isFadingOut}
$css={css`
padding-top: 0px;
`}
/>
);
}
if (readOnly) {
return (
<BlockNoteReader
initialContent={provider.document.getXmlFragment('document-store')}
docId={doc.id}
/>
);
}
return <BlockNoteEditor doc={doc} provider={provider} />;
};

View File

@@ -1,6 +1,5 @@
import { CommentBody, ThreadStore } from '@blocknote/core/comments';
import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs/doc-management';
@@ -18,13 +17,6 @@ import {
type ServerThreadListResponse = ServerThread[];
/**
* notifySubscribers generate a transaction, to distinguish
* the origin of the update, we use a specific origin "commentMarkUpdate"
* for the updates coming from the comment mark changes.
*/
export const COMMENT_UPDATE_ORIGIN = 'commentMarkUpdate';
export class DocsThreadStore extends ThreadStore {
protected static COMMENTS_PING = 'commentsPing';
protected threads: Map<string, ClientThreadData> = new Map();
@@ -32,7 +24,6 @@ export class DocsThreadStore extends ThreadStore {
(threads: Map<string, ClientThreadData>) => void
>();
private awareness?: Awareness;
private yDoc?: Y.Doc;
private lastPingAt = 0;
private pingTimer?: ReturnType<typeof setTimeout>;
@@ -40,13 +31,11 @@ export class DocsThreadStore extends ThreadStore {
protected docId: Doc['id'],
awareness: Awareness | undefined,
protected docAuth: DocsThreadStoreAuth,
yDoc?: Y.Doc,
) {
super(docAuth);
if (docAuth.canSee) {
this.awareness = awareness;
this.yDoc = yDoc;
this.awareness?.on('update', this.onAwarenessUpdate);
this.refreshThreads();
@@ -78,7 +67,7 @@ export class DocsThreadStore extends ThreadStore {
continue;
}
const state:
const state = states.get(clientId) as
| {
[DocsThreadStore.COMMENTS_PING]?: {
at: number;
@@ -87,7 +76,7 @@ export class DocsThreadStore extends ThreadStore {
threadId: string;
};
}
| undefined = states.get(clientId);
| undefined;
const ping = state?.commentsPing;
@@ -145,30 +134,18 @@ export class DocsThreadStore extends ThreadStore {
}
/**
* Notifies all subscribers about the current thread state.
* We trigger the transaction with a specific origin so we will be able
* to flag that the update comes from a comment update.
* The inner ydoc.transact calls from y-prosemirror will see there's already
* an active transaction and reuse it.
* Notifies all subscribers about the current thread state
*/
private notifySubscribers() {
// Always emit a new Map reference to help consumers detect changes
const threads = new Map(this.threads);
const notify = () => {
this.subscribers.forEach((cb) => {
try {
cb(threads);
} catch (e) {
console.warn('DocsThreadStore subscriber threw', e);
}
});
};
if (this.yDoc) {
this.yDoc.transact(notify, COMMENT_UPDATE_ORIGIN);
} else {
notify();
}
this.subscribers.forEach((cb) => {
try {
cb(threads);
} catch (e) {
console.warn('DocsThreadStore subscriber threw', e);
}
});
}
private upsertClientThreadData(thread: ClientThreadData) {

View File

@@ -1,11 +1,11 @@
import { createGlobalStyle, css } from 'styled-components';
import { css } from 'styled-components';
export const DocsCommentsStyle = createGlobalStyle<{
canSeeComment: boolean;
currentUserAvatarUrl?: string;
}>`
.--docs--main-editor.bn-root,
.--docs--main-editor.bn-root .ProseMirror {
export const cssComments = (
canSeeComment: boolean,
currentUserAvatarUrl?: string,
) => css`
& .--docs--main-editor,
& .--docs--main-editor .ProseMirror {
// Comments marks in the editor
.bn-editor {
// Resets blocknote comments styles
@@ -14,31 +14,30 @@ export const DocsCommentsStyle = createGlobalStyle<{
background-color: transparent;
}
${({ canSeeComment }) =>
canSeeComment &&
css`
.bn-thread-mark:not([data-orphan='true']) {
background-color: color-mix(
in srgb,
var(--c--contextuals--background--palette--yellow--tertiary) 40%,
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
);
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;
@@ -83,8 +82,6 @@ export const DocsCommentsStyle = createGlobalStyle<{
.bn-thread-comment {
padding: 8px;
flex-wrap: nowrap;
gap: 0px;
& .bn-editor {
padding-left: var(--c--globals--spacings--lg);
@@ -108,14 +105,10 @@ export const DocsCommentsStyle = createGlobalStyle<{
// Top bar (Name / Date / Actions) when actions displayed
&:has(.bn-comment-actions) {
& > .mantine-Group-root:first-child {
& > .mantine-Group-root {
max-width: 70%;
right: 0.3rem !important;
top: 0.3rem !important;
background: linear-gradient(
to left,
#fff 90%,
rgba(255, 255, 255, 0) 100%
);
}
.bn-menu-dropdown {
@@ -131,6 +124,7 @@ export const DocsCommentsStyle = createGlobalStyle<{
// Date
span.mantine-focus-auto {
display: inline-block;
}
.bn-comment-actions {
@@ -156,8 +150,7 @@ export const DocsCommentsStyle = createGlobalStyle<{
}
// Actions button edit comment
.bn-root + .bn-comment-actions-wrapper {
margin-top: var(--c--globals--spacings--2xs);
.bn-container + .bn-comment-actions-wrapper {
.bn-comment-actions {
flex-direction: row-reverse;
background: none;
@@ -208,8 +201,9 @@ export const DocsCommentsStyle = createGlobalStyle<{
width: 26px;
height: 26px;
flex: 0 0 26px;
background-image: ${({ currentUserAvatarUrl }) =>
currentUserAvatarUrl ? `url("${currentUserAvatarUrl}")` : 'none'};
background-image: ${currentUserAvatarUrl
? `url("${currentUserAvatarUrl}")`
: 'none'};
background-position: center;
background-repeat: no-repeat;
background-size: cover;

View File

@@ -27,15 +27,8 @@ export function useComments(
encodeURIComponent(user?.full_name || ''),
canComment,
),
provider?.document,
);
}, [
docId,
canComment,
provider?.awareness,
provider?.document,
user?.full_name,
]);
}, [docId, canComment, provider?.awareness, user?.full_name]);
useEffect(() => {
if (canComment) {

View File

@@ -41,7 +41,7 @@ export const LinkSelected = ({
const router = useRouter();
const href = `/docs/${docId}/`;
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
// If ctrl or command is pressed, it opens a new tab. If shift is pressed, it opens a new window
@@ -53,7 +53,7 @@ export const LinkSelected = ({
};
// This triggers on middle-mouse click
const handleAuxClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const handleAuxClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.button !== 1) {
return;
}
@@ -66,7 +66,6 @@ export const LinkSelected = ({
<BoxButton
as="span"
className="--docs--interlinking-link-inline-content"
data-href={href}
onClick={handleClick}
onAuxClick={handleAuxClick}
draggable="false"

View File

@@ -39,13 +39,7 @@ const withMultiColumnNoDropHandler = <
});
};
type ModulesXL =
| (Omit<typeof XLMultiColumn, 'withMultiColumn'> & {
withMultiColumn: typeof withMultiColumnNoDropHandler;
})
| undefined;
let modulesXL: ModulesXL = undefined;
let modulesXL = undefined;
if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
modulesXL = {
...XLMultiColumn,
@@ -53,4 +47,10 @@ if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
};
}
export default modulesXL;
type ModulesXL =
| (Omit<typeof XLMultiColumn, 'withMultiColumn'> & {
withMultiColumn: typeof withMultiColumnNoDropHandler;
})
| undefined;
export default modulesXL as ModulesXL;

View File

@@ -2,7 +2,6 @@ import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as Y from 'yjs';
import { COMMENT_UPDATE_ORIGIN } from '@/docs/doc-editor/components/comments/DocsThreadStore';
import { useDocContentUpdate } from '@/docs/doc-management/api/useDocContentUpdate';
import { useProviderStore } from '@/docs/doc-management/stores/useProviderStore';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning/api/useDocVersions';
@@ -66,16 +65,6 @@ export const useSaveDoc = (docId: string, yDoc: Y.Doc) => {
const isAIChange =
!transaction.local && transactionOrigin !== PROVIDER_ORIGIN_CONSTRUCTOR;
/**
* notifySubscribers generate a transaction that can be
* interpreted as a local change.
* We intercept the update with this origin to
* avoid marking the change as local.
*/
if (transaction.origin === COMMENT_UPDATE_ORIGIN) {
return;
}
setIsLocalChange(transaction.local || isAIChange);
};

View File

@@ -1,306 +1,266 @@
import { createGlobalStyle } from 'styled-components';
import { css } from 'styled-components';
export const DocsEditorStyle = createGlobalStyle`
.bn-root {
export const cssEditor = css`
.mantine-Menu-itemLabel,
.mantine-Button-label {
font-family: var(--c--components--button--font-family);
}
&,
& > .bn-container,
& .ProseMirror {
height: 100%;
}
.bn-editor {
height: 100%;
}
.mantine-Menu-itemLabel,
.mantine-Button-label {
font-family: var(--c--components--button--font-family);
}
/**
* Token Mantine
*/
/**
* Token Mantime
*/
& > .bn-container {
--bn-colors-editor-text: var(
--c--contextuals--content--semantic--neutral--primary
);
--bn-colors-side-menu: var(
--c--contextuals--content--semantic--neutral--tertiary
);
}
/**
* Ensure long placeholder text is truncated with ellipsis
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: inherit;
height: inherit;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child) {
position: relative;
}
/**
* Ensure long placeholder text is truncated with ellipsis
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: inherit;
height: inherit;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child) {
position: relative;
}
/**
* Ensure images with unsafe URLs are not interactive
*/
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}
/**
* Ensure images with unsafe URLs are not interactive
*/
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}
/**
* Collaboration cursor styles
*/
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
/**
* Collaboration cursor styles
*/
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
left: 0px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
left: 0px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
/**
* Side menu
*/
.bn-side-menu .mantine-UnstyledButton-root svg {
color: var(
--c--contextuals--content--semantic--neutral--tertiary
) !important;
}
/**
* Side menu
*/
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 54px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
.bn-side-menu[data-block-type='divider'] {
height: 38px;
}
.bn-side-menu .mantine-UnstyledButton-root svg {
color: var(
--c--contextuals--content--semantic--neutral--tertiary
) !important;
}
/**
* Callout, Paragraph and Heading blocks
*/
.bn-block {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-outer {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block > .bn-block-content[data-background-color] {
padding: var(--c--globals--spacings--3xs) var(--c--globals--spacings--3xs);
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
.bn-inline-content {
text-decoration: none;
}
a {
color: var(--c--globals--colors--gray-600);
cursor: pointer;
}
/**
* Callout, Paragraph and Heading blocks
*/
.bn-block {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-outer {
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block > .bn-block-content[data-background-color] {
padding: var(--c--globals--spacings--3xs) var(--c--globals--spacings--3xs);
border-radius: var(--c--globals--spacings--3xs);
}
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
.bn-inline-content {
text-decoration: none;
}
.bn-default-styles h1 {
font-size: 1.875rem;
}
.bn-default-styles h2 {
font-size: 1.5rem;
}
.bn-default-styles h3 {
font-size: 1.25rem;
}
a {
color: var(--c--globals--colors--gray-600);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
.bn-toolbar {
max-width: 95vw;
}
.bn-toolbar {
max-width: 95vw;
}
/**
* Quotes
*/
blockquote {
border-left: 4px solid var(--c--globals--colors--gray-300);
font-style: italic;
}
/**
* Quotes
*/
blockquote {
border-left: 4px solid var(--c--globals--colors--gray-300);
font-style: italic;
}
/**
/**
* AI
*/
ins,
[data-type='modification'] {
background: var(--c--globals--colors--brand-100);
border-bottom: 2px solid var(--c--globals--colors--brand-300);
color: var(--c--globals--colors--brand-700);
}
ins,
[data-type='modification'] {
background: var(--c--globals--colors--brand-100);
border-bottom: 2px solid var(--c--globals--colors--brand-300);
color: var(--c--globals--colors--brand-700);
}
/**
* Divider
*/
[data-content-type='divider'] hr {
background: #d3d2cf;
margin: 1rem 0;
width: 100%;
border: 1px solid #d3d2cf;
}
.bn-side-menu[data-block-type='divider'] {
height: 38px;
}
/**
* Divider
*/
[data-content-type='divider'] hr {
background: #d3d2cf;
margin: 1rem 0;
width: 100%;
border: 1px solid #d3d2cf;
}
/**
* Checklist items
*/
.bn-block-content[data-content-type='checkListItem'] > div > input {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid
var(--c--contextuals--content--semantic--neutral--tertiary);
border-radius: 4px;
cursor: pointer;
position: relative;
align-self: center;
margin-top: 2px;
}
.bn-block-content[data-content-type='checkListItem'] > div > input:checked {
background-color: var(--c--contextuals--content--semantic--brand--tertiary);
border-color: var(--c--contextuals--content--semantic--brand--tertiary);
}
.bn-block-content[data-content-type='checkListItem']
> div
> input:checked::after {
content: 'check';
font-family: 'Material Symbols Outlined Variable', sans-serif;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--c--contextuals--content--semantic--overlay--primary);
font-size: 18px;
}
/**
* Checklist items
*/
.bn-block-content[data-content-type='checkListItem'] > div > input {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid
var(--c--contextuals--content--semantic--neutral--tertiary);
border-radius: 4px;
cursor: pointer;
position: relative;
align-self: center;
margin-top: 2px;
}
.bn-block-content[data-content-type='checkListItem'] > div > input:checked {
background-color: var(--c--contextuals--content--semantic--brand--tertiary);
border-color: var(--c--contextuals--content--semantic--brand--tertiary);
}
.bn-block-content[data-content-type='checkListItem']
> div
> input:checked::after {
content: 'check';
font-family: 'Material Symbols Outlined Variable', sans-serif;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--c--contextuals--content--semantic--overlay--primary);
font-size: 18px;
}
/**
* Headings
* Ensure consistent spacing between headings and paragraphs
*/
[data-content-type='heading'] {
--level: 1.875rem;
&[data-level='2'] {
--level: 1.5rem;
}
&[data-level='3'] {
--level: 1.25rem;
}
&[data-level='4'] {
--level: 1.125rem;
}
&[data-level='5'] {
--level: 1rem;
}
&[data-level='6'] {
--level: 0.875rem;
}
/**
* Ensure consistent spacing between headings and paragraphs
*/
& .bn-block-outer:not(:first-child) {
&:has(h1) {
margin-top: 32px;
}
&:has(h2) {
margin-top: 24px;
}
&:has(h3) {
margin-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 768px) {
& .bn-editor {
padding-right: 36px;
}
}
@media screen and (width <= 560px) {
.--docs--doc-readonly & .bn-editor {
padding-left: 10px;
}
& .bn-editor {
padding-right: 10px;
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 54px;
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
height: 40px;
}
& .bn-default-styles h1 {
font-size: 1.875rem;
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-default-styles h2 {
font-size: 1.5rem;
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-default-styles h3 {
font-size: 1.25rem;
& .bn-editor h3 {
font-size: 1.2rem;
}
& .bn-default-styles h4 {
font-size: 1.125rem;
}
& .bn-default-styles h5 {
font-size: 1rem;
}
& .bn-default-styles h6 {
font-size: 0.875rem;
}
& .bn-block-outer:not(:first-child) {
&:has(h1) {
margin-top: 32px;
}
&:has(h2) {
margin-top: 24px;
}
&:has(h3) {
margin-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 768px) {
& .bn-editor {
padding-right: 36px;
}
}
@media screen and (width <= 560px) {
.--docs--doc-readonly & .bn-editor {
padding-left: 10px;
}
& .bn-editor {
padding-right: 10px;
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
[data-content-type='heading'] {
--level: 1.6rem;
&[data-level='2'] {
--level: 1.35rem;
}
&[data-level='3'] {
--level: 1.2rem;
}
&[data-level='4'] {
--level: 1.125rem;
}
&[data-level='5'] {
--level: 1rem;
}
&[data-level='6'] {
--level: 0.875rem;
}
}
& .bn-editor h1 {
font-size: 1.6rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
`;

View File

@@ -1,9 +1,3 @@
const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
export const sanitizeColor = (color: string): string => {
return HEX_COLOR_REGEX.test(color) ? color : randomColor();
};
export const randomColor = () => {
const randomInt = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;

View File

@@ -11,7 +11,7 @@ import {
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { DocumentProps, pdf } from '@react-pdf/renderer';
import jsonemoji from 'emoji-datasource-apple' with { type: 'json' };
import jsonemoji from 'emoji-datasource-apple' assert { type: 'json' };
import i18next from 'i18next';
import JSZip from 'jszip';
import { cloneElement, isValidElement, useState } from 'react';

View File

@@ -16,4 +16,6 @@ if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
};
}
export default modulesExport;
type ModulesExport = typeof modulesExport;
export default modulesExport as ModulesExport;

View File

@@ -28,8 +28,7 @@ const PRINT_ONLY_CONTENT_CSS = `
[role="contentinfo"],
div[data-is-empty-and-focused="true"],
div[data-floating-ui-focusable],
.collaboration-cursor-custom__base,
.c__toast__container
.collaboration-cursor-custom__base
{
display: none !important;
}
@@ -85,7 +84,7 @@ const PRINT_ONLY_CONTENT_CSS = `
/* Ensure BlockNote content flows properly */
.bn-editor,
.bn-root,
.bn-container,
.--docs--main-editor,
.bn-block-outer {
height: auto !important;
@@ -244,50 +243,6 @@ function wrapMediaWithLink() {
};
}
/**
* Wraps interlink inline content with anchor tags for printing,
* so they appear as clickable links in the printed PDF.
*/
function wrapInterlinksWithAnchor() {
const wrappedElements: Array<{
el: Element;
anchor: HTMLAnchorElement;
parent: Node;
}> = [];
document
.querySelectorAll('.--docs--interlinking-link-inline-content[data-href]')
.forEach((el) => {
const href = el.getAttribute('data-href');
if (!href || !isSafeUrl(href)) {
return;
}
const parent = el.parentNode;
if (!parent) {
return;
}
const anchor = document.createElement('a');
anchor.href = href;
anchor.target = '_blank';
anchor.rel = 'noopener noreferrer';
anchor.setAttribute('data-print-link', 'true');
parent.insertBefore(anchor, el);
anchor.appendChild(el);
wrappedElements.push({ el, anchor, parent });
});
return () => {
wrappedElements.forEach(({ el, anchor, parent }) => {
parent.insertBefore(el, anchor);
anchor.remove();
});
};
}
export function printDocumentWithStyles() {
if (typeof window === 'undefined') {
return;
@@ -298,9 +253,7 @@ export function printDocumentWithStyles() {
// Small delay to ensure styles are applied
setTimeout(() => {
const cleanupLinks = wrapMediaWithLink();
const cleanupInterlinks = wrapInterlinksWithAnchor();
const cleanup = () => {
cleanupInterlinks();
cleanupLinks();
cleanupPrintStyles();
};

View File

@@ -68,7 +68,7 @@ export const BoutonShare = ({
/>
}
onClick={(e) => {
addLastFocus(e.currentTarget);
addLastFocus(e.currentTarget as HTMLElement);
open();
}}
size="medium"
@@ -85,7 +85,7 @@ export const BoutonShare = ({
color="brand"
variant="tertiary"
onClick={(e) => {
addLastFocus(e.currentTarget);
addLastFocus(e.currentTarget as HTMLElement);
open();
}}
size="medium"

View File

@@ -153,40 +153,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
}
};
const insertPlainText = (plainText: string, target: HTMLElement) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
if (!target.contains(range.commonAncestorContainer)) {
target.focus();
range.selectNodeContents(target);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
range.deleteContents();
range.insertNode(document.createTextNode(plainText));
};
const handlePaste = (event: React.ClipboardEvent<HTMLSpanElement>) => {
event.preventDefault();
insertPlainText(
event.clipboardData.getData('text/plain'),
event.currentTarget,
);
};
const handleDrop = (event: React.DragEvent<HTMLSpanElement>) => {
event.preventDefault();
insertPlainText(
event.dataTransfer.getData('text/plain'),
event.currentTarget,
);
};
useEffect(() => {
setTitleDisplay(isTopRoot ? doc.title : titleWithoutEmoji);
}, [doc.title, isTopRoot, titleWithoutEmoji]);
@@ -215,8 +181,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}
onPasteCapture={handlePaste}
onDropCapture={handleDrop}
$padding={{ right: 'big' }}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {

View File

@@ -244,7 +244,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<Icon iconName="download" $color="inherit" aria-hidden={true} />
}
onClick={(e) => {
addLastFocus(e.currentTarget);
addLastFocus(e.currentTarget as HTMLElement);
setIsModalExportOpen(true);
}}
size={isSmallMobile ? 'small' : 'medium'}

View File

@@ -1,3 +1,4 @@
export * from './useCollaboration';
export * from './useCopyDocLink';
export * from './useCreateChildDocTree';
export * from './useDocTitleUpdate';

View File

@@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useCollaborationUrl } from '@/core/config';
import { KEY_DOC } from '@/docs/doc-management/api/useDoc';
import {
KEY_DOC_CONTENT,
useDocContent,
@@ -11,15 +10,13 @@ import { useProviderStore } from '@/docs/doc-management/stores/useProviderStore'
import { useIsOffline } from '@/features/service-worker/hooks/useOffline';
import { useBroadcastStore } from '@/stores/useBroadcastStore';
import { KEY_DOC } from '../api';
export const useCollaboration = (room: string) => {
const collaborationUrl = useCollaborationUrl(room);
const { addTask } = useBroadcastStore();
const queryClient = useQueryClient();
const {
setBroadcastProvider,
cleanupBroadcast,
provider: broadcastProvider,
} = useBroadcastStore();
const { setBroadcastProvider, cleanupBroadcast } = useBroadcastStore();
const {
provider,
createProvider,
@@ -68,7 +65,7 @@ export const useCollaboration = (room: string) => {
* when the document visibility changes.
*/
useEffect(() => {
if (!room || broadcastProvider?.document?.guid !== room) {
if (!room || !isReady) {
return;
}
@@ -77,7 +74,7 @@ export const useCollaboration = (room: string) => {
queryKey: [KEY_DOC, { id: room }],
});
});
}, [addTask, room, queryClient, broadcastProvider?.document?.guid]);
}, [addTask, room, queryClient, isReady]);
/**
* Set the provider when the collaboration URL and the document content are available.

View File

@@ -14,7 +14,7 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { Heading } from './Heading';
export const TableContent = ({ selector }: { selector: string }) => {
export const TableContent = () => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [containerHeight, setContainerHeight] = useState('100vh');
const { headings } = useHeadingStore();
@@ -27,29 +27,11 @@ export const TableContent = ({ selector }: { selector: string }) => {
* Calculate container height based on the scrollable content
*/
useEffect(() => {
const layout = document.querySelector<HTMLElement>(selector);
if (!layout) {
return;
const mainLayout = document.getElementById(MAIN_LAYOUT_ID);
if (mainLayout) {
setContainerHeight(`${mainLayout.scrollHeight}px`);
}
let timeout: ReturnType<typeof setTimeout>;
const updateHeight = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
setContainerHeight(`${layout.scrollHeight}px`);
}, 300);
};
updateHeight();
const observer = new ResizeObserver(updateHeight);
observer.observe(layout);
return () => {
clearTimeout(timeout);
observer.disconnect();
};
}, [selector]);
}, []);
const onOpen = () => {
setIsOpen(true);

View File

@@ -274,16 +274,11 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
/* Remove outline from TreeViewItem wrapper elements */
.c__tree-view--row {
outline: none !important;
pointer-events: initial;
&:focus-visible {
outline: none !important;
}
}
.c__tree-view--node {
pointer-events: inherit;
}
.c__tree-view--container {
z-index: 1;
margin-top: -10px;

View File

@@ -85,14 +85,15 @@ export const DocVersionEditor = ({
return (
<DocEditorContainer
docHeader={<DocVersionHeader />}
docEditor={
<BlockNoteReader
initialContent={initialContent}
docId={version.id}
isMainEditor={false}
/>
}
isDeletedDoc={false}
readOnly={true}
>
<BlockNoteReader
initialContent={initialContent}
docId={version.id}
isMainEditor={false}
/>
</DocEditorContainer>
/>
);
};

View File

@@ -50,7 +50,7 @@ export const DocsGridItemSharedButton = ({ doc, disabled }: Props) => {
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
addLastFocus(event.currentTarget);
addLastFocus(event.currentTarget as HTMLElement);
shareModal.open();
}}
color="brand"

View File

@@ -109,7 +109,6 @@ export const useImport = ({ onDragOver }: UseImportProps) => {
});
},
noClick: true,
noKeyboard: true,
});
const { mutate: importDoc, isPending } = useImportDoc();

View File

@@ -2,9 +2,7 @@ import {
ModalSize,
OnboardingModal,
type OnboardingModalProps,
OnboardingStep,
} from '@gouvfr-lasuite/ui-kit';
import React, { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
@@ -12,29 +10,9 @@ import { useConfig } from '@/core/config/api/useConfig';
import { useOnboardingSteps } from '../hooks/useOnboardingSteps';
/**
* typing was not correct on ui-kit side for the description prop of OnboardingStep,
* it can be a string or a ReactNode but was typed as string only, so we need to override the
* type here to be able to use ReactNode
*/
type OnboardingStepFixed = Omit<OnboardingStep, 'description'> & {
description?: ReactNode;
};
type OnboardingModalPropsFixed = Omit<OnboardingModalProps, 'steps'> & {
steps?: OnboardingStepFixed[];
};
const OnboardingModalFixed =
OnboardingModal as React.ComponentType<OnboardingModalPropsFixed>;
const OnBoardingStyle = createGlobalStyle`
.c__onboarding-modal__steps{
height: auto;
& a{
color:inherit;
}
}
.c__onboarding-modal__content {
height: 350px;
@@ -54,7 +32,7 @@ const OnBoardingStyle = createGlobalStyle`
*:not(.material-icons):not(.material-icons-filled):not(
.material-symbols-outlined
) {
font-family: var(--c--globals--font--families--base);
font-family: Marianne, Inter, Roboto Flex Variable, sans-serif;
}
/* Separator between content and footer actions/link */
@@ -78,10 +56,6 @@ const OnBoardingStyle = createGlobalStyle`
display: flex;
flex-direction: column;
a{
color: inherit;
}
& .c__onboarding-modal__body{
justify-content: center;
}
@@ -107,7 +81,7 @@ export const OnBoarding = (props: OnBoardingProps) => {
return (
<>
{props.isOpen ? <OnBoardingStyle /> : null}
<OnboardingModalFixed
<OnboardingModal
size={ModalSize.LARGE}
appName={t('Discover Docs')}
mainTitle={t('Learn the core principles')}

View File

@@ -1,8 +1,7 @@
import { type OnboardingStep } from '@gouvfr-lasuite/ui-kit';
import Image from 'next/image';
import { Trans, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import DragIndicatorIcon from '../assets/drag_indicator.svg';
@@ -17,9 +16,6 @@ export interface OnboardingStepsData {
export const useOnboardingSteps = () => {
const { t } = useTranslation();
const { data: config } = useConfig();
const readyTemplateUrl =
config?.theme_customization?.onboarding?.ready_template_url;
const { contextualTokens, colorsTokens } = useCunninghamTheme();
const activeColor =
contextualTokens.content.semantic.brand.tertiary ??
@@ -126,21 +122,8 @@ export const useOnboardingSteps = () => {
</OnboardingStepIcon>
),
title: t('Draw inspiration from the content library'),
description: (
<Trans
t={t}
i18nKey="Start from <Link>ready-made templates</Link> for common use cases, then customize them to match your workflow in minutes."
components={{
Link: (
<a
target="_blank"
rel="noopener noreferrer"
href={readyTemplateUrl}
aria-label={t('Ready-made templates (opens in a new tab)')}
/>
),
}}
/>
description: t(
'Start from ready-made templates for common use cases, then customize them to match your workflow in minutes.',
),
content: (
<Image

View File

@@ -50,7 +50,7 @@ export class RequestSerializer {
public static arrayBufferToString(buffer: ArrayBufferLike) {
const decoder = new TextDecoder();
return decoder.decode(buffer);
return decoder.decode(buffer as ArrayBuffer);
}
public static arrayBufferToJson<T>(buffer: ArrayBufferLike) {

View File

@@ -15,7 +15,7 @@ const mockServiceWorkerScope = {
(global as any).self = {
...global,
clients: mockServiceWorkerScope.clients,
};
} as unknown as ServiceWorkerGlobalScope;
describe('OfflinePlugin', () => {
afterEach(() => vi.clearAllMocks());

View File

@@ -1,12 +1,13 @@
import { css, keyframes } from 'styled-components';
import { Box, BoxType } from '@/components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useResponsiveStore } from '@/stores';
import { SkeletonCircle, SkeletonLine } from './SkeletionUI';
export const DocEditorSkeleton = () => {
const { isDesktop } = useResponsiveStore();
const { spacingsTokens } = useCunninghamTheme();
return (
<>
{/* Main Editor Container */}
@@ -16,117 +17,80 @@ export const DocEditorSkeleton = () => {
$height="100%"
className="--docs--doc-editor-skeleton"
>
<SkeletonEditorHeader />
<SkeletonEditorCore />
{/* Header Skeleton */}
<Box
$padding={{ horizontal: isDesktop ? '70px' : 'base' }}
className="--docs--doc-editor-header-skeleton"
>
<Box
$width="100%"
$padding={{ top: isDesktop ? '65px' : 'md' }}
$gap={spacingsTokens['base']}
>
<Box
$direction="row"
$align="center"
$width="100%"
$padding={{ bottom: 'xs' }}
>
<Box
$direction="row"
$justify="space-between"
$css="flex:1;"
$gap="0.5rem 1rem"
$align="center"
$maxWidth="100%"
>
{/* Title and metadata skeleton */}
<Box $gap="0.25rem" $css="flex:1;">
{/* Title - "Untitled Document" style */}
<SkeletonLine $width="35%" $height="40px" />
{/* Metadata (role and last update) */}
<Box $direction="row" $gap="0.5rem" $align="center">
<SkeletonLine $maxWidth="260px" $height="12px" />
</Box>
</Box>
{/* Toolbox skeleton (buttons) */}
<Box $direction="row" $gap="0.75rem" $align="center">
{/* Share button */}
<SkeletonLine $width="90px" $height="40px" />
{/* Download icon */}
<SkeletonCircle $width="40px" $height="40px" />
{/* Menu icon */}
<SkeletonCircle $width="40px" $height="40px" />
</Box>
</Box>
</Box>
{/* Separator */}
<SkeletonLine $height="1px" />
</Box>
</Box>
{/* Content Skeleton */}
<Box
$direction="row"
$width="100%"
$css="overflow-x: clip; flex: 1;"
$position="relative"
className="--docs--doc-editor-content-skeleton"
>
<Box
$css="flex:1;"
$position="relative"
$width="100%"
$padding={{ horizontal: isDesktop ? '70px' : 'base', top: 'lg' }}
>
{/* Placeholder text similar to screenshot */}
<Box $gap="0rem">
{/* Single placeholder line like in the screenshot */}
<SkeletonLine $width="85%" $height="20px" />
</Box>
</Box>
</Box>
</Box>
</>
);
};
const SkeletonEditorHeader = () => {
const { isDesktop } = useResponsiveStore();
const { spacingsTokens } = useCunninghamTheme();
return (
<Box
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
className="--docs--doc-editor-header-skeleton"
>
<Box
$width="100%"
$padding={{ top: isDesktop ? '65px' : 'md' }}
$gap={spacingsTokens['base']}
>
<Box
$direction="row"
$align="center"
$width="100%"
$padding={{ bottom: 'xs' }}
>
<Box
$direction="row"
$justify="space-between"
$css="flex:1;"
$gap="0.5rem 1rem"
$align="center"
$maxWidth="100%"
>
{/* Title and metadata skeleton */}
<Box $gap="0.25rem" $css="flex:1;">
{/* Title - "Untitled Document" style */}
<SkeletonLine $width="35%" $height="40px" />
{/* Metadata (role and last update) */}
<Box $direction="row" $gap="0.5rem" $align="center">
<SkeletonLine $maxWidth="260px" $height="12px" />
</Box>
</Box>
{/* Toolbox skeleton (buttons) */}
<Box $direction="row" $gap={spacingsTokens['t']} $align="center">
{/* Share button */}
<SkeletonLine $width="90px" $height="40px" />
{/* Download icon */}
<SkeletonCircle $width="40px" $height="40px" />
{/* Menu icon */}
<SkeletonCircle $width="40px" $height="40px" />
</Box>
</Box>
</Box>
{/* Separator */}
<SkeletonLine $height="1px" />
</Box>
</Box>
);
};
export const SKELETON_FADE_DURATION_MS = 150;
const skeletonFadeOut = keyframes`
from { opacity: 1; }
to { opacity: 0; }
`;
type SkeletonEditorCoreProps = Partial<BoxType> & {
isFadingOut?: boolean;
};
export const SkeletonEditorCore = ({
isFadingOut,
$css,
...props
}: SkeletonEditorCoreProps) => {
const { isDesktop } = useResponsiveStore();
return (
<Box
$direction="row"
$width="100%"
$css="overflow-x: clip; flex: 1;"
$position="relative"
className="--docs--doc-editor-content-skeleton"
>
<Box
$position="relative"
$width="100%"
$padding={{ horizontal: isDesktop ? '54px' : 'base', top: 'md' }}
$flex="1"
$css={css`
${$css}
${isFadingOut &&
css`
animation: ${skeletonFadeOut} ${SKELETON_FADE_DURATION_MS}ms
ease-in-out forwards;
`}
`}
{...props}
>
<Box $gap="1.5rem">
<SkeletonLine $width="65%" $height="35px" />
<SkeletonLine $width="55%" $height="25px" />
<SkeletonLine $width="35%" $height="20px" />
</Box>
</Box>
</Box>
);
};

View File

@@ -1,24 +0,0 @@
import { useEffect, useState } from 'react';
import { SKELETON_FADE_DURATION_MS } from '../components/DocEditorSkeleton';
export const useSkeletonFadeOut = (showContent: boolean) => {
const [skeletonVisible, setSkeletonVisible] = useState(!showContent);
const [isFadingOut, setIsFadingOut] = useState(false);
useEffect(() => {
if (showContent) {
setIsFadingOut(true);
const timer = setTimeout(
() => setSkeletonVisible(false),
SKELETON_FADE_DURATION_MS,
);
return () => clearTimeout(timer);
} else {
setSkeletonVisible(true);
setIsFadingOut(false);
}
}, [showContent]);
return { skeletonVisible, isFadingOut };
};

View File

@@ -36,6 +36,7 @@ if (!isInitialized && !i18next.isInitialized) {
lowerCaseLng: true,
nsSeparator: false,
keySeparator: false,
showSupportNotice: false,
})
.then(() => {
if (typeof document !== 'undefined') {

View File

@@ -629,7 +629,6 @@
"Change role for {{name}}": "Αλλαγή ρόλου για {{name}}",
"Checklist applied": "Εφαρμόστηκε λίστα ελέγχου",
"Choose a user": "Επιλέξτε ένα χρήστη",
"Choose the email": "Επιλέξτε το email",
"Choose the new location for <strong>{{title}}</strong>.": "Επιλέξτε τη νέα θέση για το <strong>{{title}}</strong>.",
"Close the access request modal": "Κλείσιμο παραθύρου αίτησης πρόσβασης",
"Close the delete modal": "Κλείσιμο παραθύρου διαγραφής",
@@ -687,15 +686,12 @@
"Document tree": "Δομή εγγράφου",
"Document unpinned successfully!": "Το έγγραφο ξεκαρφιτσώθηκε επιτυχώς!",
"Document visibility": "Ορατότητα εγγράφου",
"Document {{activeId}} is over document {{overId}}.": "Το έγγραφο {{activeId}} είναι πάνω από το έγγραφο {{overId}}.",
"Document {{id}} was dropped.": "Το έγγραφο {{id}} απορρίφθηκε.",
"Documents grid": "Πλέγμα εγγράφων",
"Docx": "Docx",
"Download": "Λήψη",
"Download anyway": "Λήψη οπωσδήποτε",
"Download {{format}}": "Λήψη σε {{format}}",
"Drag and drop status": "Κατάσταση μεταφοράς και απόθεσης",
"Dragging was cancelled. Document {{id}} was dropped.": "Ακυρώθηκε η απόσυρση. Το έγγραφο {{id}} απορρίφθηκε.",
"Draw inspiration from the content library": "Αντλήστε έμπνευση από τη βιβλιοθήκη περιεχομένου",
"Duplicate": "Δημιουργία αντιγράφου",
"Edit document emoji": "Επεξεργασία emoji εγγράφου",
@@ -790,7 +786,6 @@
"New sub-doc": "Νέο υπο-έγγραφο",
"No document found": "Δεν βρέθηκε έγγραφο",
"No documents found": "Δεν βρέθηκαν έγγραφα",
"No results. Type a full email address to invite someone.": "Δεν υπάρχουν αποτελέσματα. Πληκτρολογήστε μια πλήρη διεύθυνση email για να προσκαλέσετε κάποιον.",
"No text selected": "Δεν επιλέχθηκε κείμενο",
"No versions": "Δεν υπάρχουν εκδόσεις",
"Numbered list applied": "Εφαρμόστηκε αριθμημένη λίστα",
@@ -818,7 +813,6 @@
"Paragraph applied": "Εφαρμόστηκε παράγραφος",
"Pending invitations": "Εκκρεμείς προσκλήσεις",
"People with access via the parent document": "Άτομα με πρόσβαση μέσω του γονικού εγγράφου",
"Picked up document {{id}}.": "Επιλέχθηκε το έγγραφο {{id}}.",
"Pin": "Καρφίτσωμα",
"Pinned documents": "Καρφιτσωμένα έγγραφα",
"Please download it only if it comes from a trusted source.": "Παρακαλούμε πραγματοποιήστε λήψη μόνο εάν προέρχεται από αξιόπιστη πηγή.",
@@ -879,7 +873,6 @@
"Summarize": "Σύνοψη",
"Summary": "Περίληψη",
"The antivirus has detected an anomaly in your file.": "Το λογισμικό προστασίας από ιούς εντόπισε μια ανωμαλία στο αρχείο σας.",
"The current document will be replaced, but you'll still find it in the version history.": "Το τρέχον έγγραφο θα αντικατασταθεί, αλλά θα το βρείτε ακόμα στο ιστορικό εκδόσεων.",
"The document \"{{documentName}}\" has been successfully imported": "Το έγγραφο \"{{documentName}}\" εισήχθη επιτυχώς",
"The document \"{{documentName}}\" import has failed": "Η εισαγωγή του εγγράφου \"{{documentName}}\" απέτυχε",
"The document \"{{documentName}}\" import has failed (only .docx and .md files are allowed)": "Η εισαγωγή του εγγράφου \"{{documentName}}\" απέτυχε (επιτρέπονται μόνο αρχεία .docx και .md)",
@@ -902,7 +895,6 @@
"Too many requests. Please wait 60 seconds.": "Πάρα πολλά αιτήματα. Παρακαλούμε περιμένετε 60 δευτερόλεπτα.",
"Trashbin": "Κάδος απορριμμάτων",
"Type a name or email": "Πληκτρολογήστε όνομα ή email",
"Type at least {{minLength}} characters to display user names": "Πληκτρολογήστε τουλάχιστον {{minLength}} χαρακτήρες για την εμφάνιση ονομάτων χρηστών",
"Type the name of a document": "Πληκτρολογήστε το όνομα ενός εγγράφου",
"Unpin": "Ξεκαρφίτσωμα",
"Untitled document": "Έγγραφο χωρίς τίτλο",
@@ -1199,7 +1191,6 @@
"An error occurred...": "Une erreur s'est produite...",
"An uncompromising writing experience.": "Une expérience d'écriture sans compromis.",
"An unexpected error occurred.": "Une erreur inattendue sest produite.",
"An unexpected error occurred. Go grab a coffee or try to refresh the page.": "Une erreur inattendue est survenue. Allez prendre un café ou essayez d'actualiser la page.",
"Analyzing file...": "Analyse du fichier...",
"Anonymous": "Anonyme",
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
@@ -1289,7 +1280,6 @@
"Document {{activeId}} was dropped over document {{overId}}.": "Le document {{activeId}} a été abandonné sur le document {{overId}}.",
"Document {{id}} is no longer over a droppable area.": "Le document {{id}} n'est plus au-dessus d'une zone dépotable.",
"Document {{id}} was dropped.": "Le document {{id}} a été abandonné.",
"Documentation": "Documentation",
"Documents grid": "Grille des documents",
"Docx": "Docx",
"Download": "Télécharger",
@@ -1325,7 +1315,6 @@
"Flexible export.": "Un export flexible.",
"Format": "Format",
"Format your content with the toolbar": "Formatez votre contenu avec la barre d'outils",
"Get Support": "Obtenir de l'aide",
"Go to content": "Voir le contenu",
"Govs ❤️ Open Source.": "Gouvernements ❤️ Open Source.",
"HTML": "HTML",
@@ -1443,7 +1432,6 @@
"Request access modal": "Demande d'accès",
"Reset": "Réinitialiser",
"Reset search filters": "Réinitialiser les filtres de recherche",
"Resize sidebar": "Redimensionner la barre latérale",
"Restore": "Restaurer",
"Restore version of {{date}}": "Restaurer la version de {{date}}",
"Restoring an older version": "Restauration d'une ancienne version en cours",
@@ -1452,7 +1440,6 @@
"Search docs": "Rechercher des docs",
"Search documents": "Rechercher des documents",
"Search for a doc": "Rechercher un document",
"Search for a document": "Rechercher un document",
"Search modal": "Modale de recherche",
"Search results": "Résultats de la recherche",
"Select a document": "Sélectionnez un document",
@@ -1475,14 +1462,11 @@
"Show more": "Voir plus",
"Show the side panel for {{title}}": "Afficher le panneau latéral pour {{title}}",
"Show the table of contents": "Afficher la table des matières",
"Sidebar width: medium": "Largeur de la barre latérale : moyenne",
"Sidebar width: narrow": "Largeur de la barre latérale: étroite",
"Sidebar width: wide": "Largeur de la barre latérale: large",
"Simple and secure collaboration.": "Une collaboration simple et sécurisée.",
"Simple document icon": "Icône simple du document",
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
"Start Writing": "Commencer à écrire",
"Start from <Link>ready-made templates</Link> for common use cases, then customize them to match your workflow in minutes.": "Commencez à partir de <Link>modèles prêts à l'emploi</Link> pour les cas d'utilisation courants, puis personnalisez-les pour correspondre à votre flux de travail en quelques minutes.",
"Start from ready-made templates for common use cases, then customize them to match your workflow in minutes.": "Commencez à partir de modèles prêts à l'emploi pour les cas d'utilisation courants, puis personnalisez-les pour correspondre à votre flux de travail en quelques minutes.",
"Stop": "Arrêter",
"Summarize": "Résumer",
"Summary": "Sommaire",
@@ -1548,7 +1532,6 @@
"home-content-open-source-part2": "Vous pouvez facilement auto-héberger Docs (consultez notre <2>documentation</2> d'installation).<br/>Docs utilise une <7>licence</7> (MIT) adaptée à l'innovation et aux entreprises.<br/>Les contributions sont les bienvenues (consultez notre feuille de route <13>ici</13>).",
"home-content-open-source-part3": "Docs est le résultat d'un effort conjoint mené par les gouvernements français 🇫🇷🥖 <1>(DINUM)</1> et allemand 🇩🇪🥨 <5>(ZenDiS)</5>.",
"just now": "à l'instant",
"mention a sub-doc...": "mentionner un sous-document...",
"new window": "nouvelle fenêtre",
"pdf": "pdf",
"src_img_onboarding_step_1": "/assets/on-boarding/step_1_FR.gif",
@@ -2071,7 +2054,6 @@
"An error occurred...": "Произошла ошибка...",
"An uncompromising writing experience.": "Бескомпромиссный опыт написания.",
"An unexpected error occurred.": "Произошла непредвиденная ошибка.",
"An unexpected error occurred. Go grab a coffee or try to refresh the page.": "Произошла непредвиденная ошибка. Сходите за чашкой кофе или попробуйте обновить страницу.",
"Analyzing file...": "Анализ файла...",
"Anonymous": "Аноним",
"Anyone with the link can edit the document": "Любой, у кого есть ссылка, может редактировать документ",
@@ -2161,7 +2143,6 @@
"Document {{activeId}} was dropped over document {{overId}}.": "Документ {{activeId}} был сброшен поверх документа {{overId}}.",
"Document {{id}} is no longer over a droppable area.": "Документ {{id}} больше не находится над местом, куда его можно поместить.",
"Document {{id}} was dropped.": "Документ {{id}} сброшен.",
"Documentation": "Документация",
"Documents grid": "Сетка документов",
"Docx": "Docx",
"Download": "Загрузить",
@@ -2197,7 +2178,6 @@
"Flexible export.": "Полезные форматы экспорта.",
"Format": "Формат",
"Format your content with the toolbar": "Форматируйте содержимое с помощью панели инструментов",
"Get Support": "Получить поддержку",
"Go to content": "Перейти к содержимому",
"Govs ❤️ Open Source.": "Govs ❤️ Open Source.",
"HTML": "HTML",
@@ -2315,7 +2295,6 @@
"Request access modal": "Запрос доступа",
"Reset": "Сброс",
"Reset search filters": "Сбросить фильтры поиска",
"Resize sidebar": "Изменить размер панели",
"Restore": "Восстановить",
"Restore version of {{date}}": "Восстановить версию от {{date}}",
"Restoring an older version": "Восстановление более старой версии",
@@ -2324,7 +2303,6 @@
"Search docs": "Поиск документов",
"Search documents": "Поиск документов",
"Search for a doc": "Поиск документов",
"Search for a document": "Поиск документа",
"Search modal": "Поиск",
"Search results": "Результаты поиска",
"Select a document": "Выберите документ",
@@ -2347,9 +2325,6 @@
"Show more": "Показать ещё",
"Show the side panel for {{title}}": "Показать боковую панель для {{title}}",
"Show the table of contents": "Показать оглавление",
"Sidebar width: medium": "Ширина боковой панели: средняя",
"Sidebar width: narrow": "Ширина боковой панели: узкая",
"Sidebar width: wide": "Ширина боковой панели: широкая",
"Simple and secure collaboration.": "Простое и безопасное сотрудничество.",
"Simple document icon": "Простой значок документа",
"Something bad happens, please retry.": "Что-то пошло не так, повторите попытку.",
@@ -2420,7 +2395,6 @@
"home-content-open-source-part2": "Вы можете легко разместить Docs у себя (см. нашу <2>документацию по установке</2>).<br/>Docs использует <7>лицензию</7> (MIT), подходящую для инноваций и бизнеса.<br/>Мы приветствуем ваши вклады (см. наш план разработки <13>здесь</13>).",
"home-content-open-source-part3": "Docs — это результат совместных усилий правительств Франции 🇫🇷🥖 <1>(DINUM)</1> и Германии 🇩🇪🥨 <5>(ZenDiS)</5>.",
"just now": "только что",
"mention a sub-doc...": "упоминание вложенного документа...",
"new window": "новое окно",
"pdf": "pdf",
"src_img_onboarding_step_1": "src_img_onboarding_step_1",
@@ -2569,7 +2543,6 @@
"An error occurred...": "Виникла помилка...",
"An uncompromising writing experience.": "Безкомпромісне задоволення від процесу письма.",
"An unexpected error occurred.": "Сталася неочікувана помилка.",
"An unexpected error occurred. Go grab a coffee or try to refresh the page.": "Сталася неочікувана помилка. Сходіть за кавою або спробуйте оновити сторінку.",
"Analyzing file...": "Аналіз файлу...",
"Anonymous": "Анонім",
"Anyone with the link can edit the document": "Будь-хто з посиланням може редагувати документ",
@@ -2659,7 +2632,6 @@
"Document {{activeId}} was dropped over document {{overId}}.": "Документ {{activeId}} відпущений над документом {{overId}}.",
"Document {{id}} is no longer over a droppable area.": "Документ {{id}} більше не в зоні для пересування.",
"Document {{id}} was dropped.": "Документ {{id}} був відхилений.",
"Documentation": "Документація",
"Documents grid": "Сітка документів",
"Docx": "Docx",
"Download": "Завантажити",
@@ -2695,7 +2667,6 @@
"Flexible export.": "Гнучкий експорт.",
"Format": "Формат",
"Format your content with the toolbar": "Форматуйте вміст за допомогою панелі інструментів",
"Get Support": "Отримати підтримку",
"Go to content": "Перейти до вмісту",
"Govs ❤️ Open Source.": "Govs ❤️ Open Source.",
"HTML": "HTML",
@@ -2813,7 +2784,6 @@
"Request access modal": "Запит доступу",
"Reset": "Скинути",
"Reset search filters": "Скинути фільтри пошуку",
"Resize sidebar": "Змінити розмір бічної панелі",
"Restore": "Відновити",
"Restore version of {{date}}": "Відновити версію від {{date}}",
"Restoring an older version": "Відновлення старішої версії",
@@ -2822,7 +2792,6 @@
"Search docs": "Пошук документів",
"Search documents": "Пошук документів",
"Search for a doc": "Пошук документу",
"Search for a document": "Пошук документа",
"Search modal": "Пошук",
"Search results": "Результати пошуку",
"Select a document": "Оберіть документ",
@@ -2845,9 +2814,6 @@
"Show more": "Показати більше",
"Show the side panel for {{title}}": "Показувати бічну панель для {{title}}",
"Show the table of contents": "Показати зміст",
"Sidebar width: medium": "Ширина бічної панелі: середня",
"Sidebar width: narrow": "Ширина бічної панелі: вузька",
"Sidebar width: wide": "Ширина бічної панелі: широка",
"Simple and secure collaboration.": "Проста та безпечна співпраця.",
"Simple document icon": "Проста піктограма документа",
"Something bad happens, please retry.": "Сталася помилка, спробуйте ще раз.",
@@ -2918,7 +2884,6 @@
"home-content-open-source-part2": "Ви можете легко самостійно розмістити Docs (див. нашу <2>документацію з встановлення</2>).<br/>Docs використовує <7>ліцензію</7> (MIT), що підходить для інновацій та бізнесу.<br/>Внески вітаються (див. наш план розробки <13>тут</13>).",
"home-content-open-source-part3": "Docs є результатом спільних зусиль, очолюваних урядами Франції 🇫🇷🥖 <1>(DINUM)</1> та Німеччини 🇩🇪🥨 <5>(ZenDiS)</5>.",
"just now": "щойно",
"mention a sub-doc...": "згадка про вкладений документ...",
"new window": "нове вікно",
"pdf": "pdf",
"src_img_onboarding_step_1": "src_img_onboarding_step_1",

View File

@@ -8,16 +8,12 @@
body {
margin: 0;
padding: 0;
overflow: hidden;
}
/* stylelint-disable-next-line selector-id-pattern */
body > #__next > .c__app > div:has(> .c__loader) {
min-height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
}
* {

View File

@@ -109,7 +109,5 @@ export const useBroadcastStore = create<BroadcastState>((set, get) => ({
Object.values(get().tasks).forEach(({ task, observer }) => {
task.unobserve(observer);
});
set({ tasks: {}, provider: undefined });
},
}));

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2020",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "5.0.0",
"version": "4.8.6",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -31,18 +31,17 @@
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@tiptap/extensions": "3.22.4",
"@types/node": "24.12.2",
"@tiptap/extensions": "3.20.4",
"@types/node": "24.12.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"eslint": "10.2.1",
"postcss": "8.5.10",
"prosemirror-view": "1.41.8",
"react": "19.2.5",
"react-dom": "19.2.5",
"eslint": "10.1.0",
"glob": "13.0.6",
"prosemirror-view": "1.41.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"serialize-javascript": "7.0.5",
"typescript": "6.0.3",
"uuid": "14.0.0",
"typescript": "5.9.3",
"wrap-ansi": "10.0.0",
"yjs": "13.6.30"
},

View File

@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "5.0.0",
"version": "4.8.6",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -18,23 +18,23 @@
},
"dependencies": {
"@eslint/js": "10.0.1",
"@next/eslint-plugin-next": "16.2.4",
"@tanstack/eslint-plugin-query": "5.99.2",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"@typescript-eslint/utils": "8.59.0",
"@vitest/eslint-plugin": "1.6.16",
"eslint-config-next": "16.2.4",
"@next/eslint-plugin-next": "16.2.1",
"@tanstack/eslint-plugin-query": "5.95.0",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"@vitest/eslint-plugin": "1.6.13",
"eslint-config-next": "16.2.1",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-jest": "29.15.2",
"eslint-plugin-jest": "29.15.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.10.2",
"eslint-plugin-playwright": "2.10.1",
"eslint-plugin-prettier": "5.5.5",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-testing-library": "7.16.2",
"prettier": "3.8.3"
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-testing-library": "7.16.1",
"prettier": "3.8.1"
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "5.0.0",
"version": "4.8.6",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -21,7 +21,7 @@
"eslint-plugin-import": "2.32.0",
"i18next-parser": "9.4.0",
"jest": "30.3.0",
"ts-jest": "29.4.9",
"ts-jest": "29.4.6",
"typescript": "*",
"yargs": "18.0.0"
},

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2020",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "5.0.0",
"version": "4.8.6",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -16,12 +16,12 @@
"node": ">=22"
},
"dependencies": {
"@blocknote/server-util": "0.49.0",
"@blocknote/server-util": "0.47.3",
"@hocuspocus/server": "3.4.4",
"@sentry/node": "10.49.0",
"@sentry/profiling-node": "10.49.0",
"@sentry/node": "10.45.0",
"@sentry/profiling-node": "10.45.0",
"@tiptap/extensions": "*",
"axios": "1.15.2",
"axios": "1.15.0",
"cors": "2.8.6",
"express": "5.2.1",
"express-ws": "5.0.2",
@@ -30,7 +30,7 @@
"yjs": "*"
},
"devDependencies": {
"@blocknote/core": "0.49.0",
"@blocknote/core": "0.47.3",
"@hocuspocus/provider": "3.4.4",
"@types/cors": "2.8.19",
"@types/express": "5.0.6",
@@ -45,8 +45,8 @@
"ts-node": "10.9.2",
"tsc-alias": "1.8.16",
"typescript": "*",
"vitest": "4.1.4",
"vitest-mock-extended": "4.0.0",
"vitest": "4.1.0",
"vitest-mock-extended": "3.1.0",
"ws": "8.20.0"
},
"packageManager": "yarn@1.22.22"

View File

@@ -62,7 +62,7 @@ const readers: InputReader[] = [
const ydoc = new Y.Doc();
try {
Y.applyUpdate(ydoc, data);
return editor.yDocToBlocks(ydoc, 'document-store');
return editor.yDocToBlocks(ydoc, 'document-store') as PartialBlock[];
} finally {
ydoc.destroy();
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2020",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

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