Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f1122def1 | |||
| 5bac9a1d59 | |||
| 61aa3c79c5 | |||
| 0c6cd8223d | |||
| b692314646 | |||
| e816475981 | |||
| 8d1f01645a | |||
| 29a46a413e | |||
| b618b2347f | |||
| 7038f2a85d | |||
| b7dfafaf47 | |||
| 2a7d963f50 | |||
| 980117132f | |||
| 7a8b50b5f0 | |||
| 409e403581 | |||
| 0c0eed6f59 | |||
| 750b7f86b4 | |||
| 9569c58315 | |||
| 922a968418 | |||
| 48e6cef763 | |||
| ac4ec6c752 | |||
| 7454d44329 | |||
| 1e3e7de753 | |||
| 4c2eb31a6a | |||
| a2a44a6546 | |||
| f1fa99f918 | |||
| 4a6e65d4be | |||
| c8772bb1ad | |||
| d54925bd97 | |||
| e9f3e27058 | |||
| 0abbe4a26f | |||
| 3e93f5924c | |||
| d70dc41643 | |||
| 6e81b55403 | |||
| b8cc21debc | |||
| 36ddb84982 | |||
| 496ae12fa9 | |||
| ae4ef48d05 | |||
| 952104fd82 | |||
| 60dc8bf174 | |||
| d54a61cbcc | |||
| f90a1e3549 | |||
| 572b80d3fe | |||
| 82d840a15f | |||
| 500c690fa0 | |||
| 577111d864 | |||
| 563f1e4c0f | |||
| 2e8407ac7c | |||
| 2af9ec0d85 | |||
| b70799c2db | |||
| 0facfc11be | |||
| 8023e44f71 | |||
| b27c5e9b92 | |||
| 3e5c4c32e9 | |||
| 551207ab86 | |||
| 0aa47fcd1e |
@@ -249,67 +249,3 @@ jobs:
|
||||
|
||||
- name: Build SDK
|
||||
run: npm run build
|
||||
|
||||
i18n-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "infrastructure,secrets"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
- name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/meet/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
|
||||
- name: Install gettext (required to make messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install development dependencies
|
||||
working-directory: src/backend
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Generate the translation base file
|
||||
run: ~/.local/bin/django-admin makemessages --keep-pot --all
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
cache: "npm"
|
||||
cache-dependency-path: src/frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci
|
||||
|
||||
- name: Extract the frontend translation
|
||||
run: make frontend-i18n-extract
|
||||
|
||||
- name: Upload files to Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin/config.yml
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_BASE_PATH: ${{ github.workspace }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -49,7 +49,7 @@ clean_old_images('localhost:5001/meet-summary')
|
||||
# This is necessary because we need to inject the certificate into our LiveKit container
|
||||
local_resource(
|
||||
'copy-root-ca',
|
||||
cmd='cp "$(mkcert -CAROOT)/rootCA.pem" ../docker/livekit/rootCA.pem',
|
||||
cmd='cp -f "$(mkcert -CAROOT)/rootCA.pem" ../docker/livekit/rootCA.pem',
|
||||
deps=[], # No dependencies needed
|
||||
)
|
||||
# Build a custom LiveKit Docker image that includes our root CA certificate
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document is a step-by-step guide that describes how to install Visio on a k8s cluster without AI features.
|
||||
|
||||
## Prerequisites
|
||||
## Prerequisites for a kubernetes setup
|
||||
|
||||
- k8s cluster with an nginx-ingress controller
|
||||
- an OIDC provider (if you don't have one, we will provide an example)
|
||||
@@ -12,14 +12,25 @@ This document is a step-by-step guide that describes how to install Visio on a k
|
||||
|
||||
### Test cluster
|
||||
|
||||
If you do not have a test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script **bin/start-kind.sh**.
|
||||
If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**.
|
||||
|
||||
To be able to use the script, you will need to install:
|
||||
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
|
||||
|
||||
To be able to use the script, you will need to install the following components:
|
||||
|
||||
- Docker (https://docs.docker.com/desktop/)
|
||||
- Kind (https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
|
||||
- Mkcert (https://github.com/FiloSottile/mkcert#installation)
|
||||
- Helm (https://helm.sh/docs/intro/quickstart/#install-helm)
|
||||
- kubectl (https://kubernetes.io/docs/tasks/tools/)
|
||||
|
||||
In order to initiate the local kind installation via **start-kind.sh** do the following:
|
||||
1) Make sure administrator/root user context is able to execute mkcert, docker, kind etc. commands or the script might fail
|
||||
2) Download the script to the device where the above components are installed
|
||||
3) Make the script executable
|
||||
4) Run the script with proper permissions (administrator/sudo etc.)
|
||||
|
||||
The output of the script will resemble the below example:
|
||||
|
||||
```
|
||||
$ ./bin/start-kind.sh
|
||||
@@ -99,7 +110,7 @@ When your k8s cluster is ready, you can start the deployment. This cluster is sp
|
||||
|
||||
Please remember that \*.127.0.0.1.nip.io will always resolve to 127.0.0.1, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
|
||||
|
||||
## Preparation
|
||||
## Preparation of components
|
||||
|
||||
### What will you use to authenticate your users ?
|
||||
|
||||
@@ -111,7 +122,7 @@ If you haven't run the script **bin/start-kind.sh**, you'll need to manually cre
|
||||
$ kubectl create namespace meet
|
||||
```
|
||||
|
||||
If you have already run the script, you can skip this step and proceed to the next instruction.
|
||||
If you have already run the script, you can skip this step and proceed to the next instruction. NOTE: Before you proceed, and is using the kind method, make sure you download this repo examples/ directory and its contents to the location where you will be executing the helm command. Helm will look for "examples/<name>values.yaml" from based on the path it is being executed.
|
||||
|
||||
```
|
||||
$ kubectl config set-context --current --namespace=meet
|
||||
@@ -232,7 +243,7 @@ meet <none> meet.127.0.0.1.nip.io localhost 80, 44
|
||||
meet-admin <none> meet.127.0.0.1.nip.io localhost 80, 443 52m
|
||||
```
|
||||
|
||||
You can use Visio on https://meet.127.0.0.1.nip.io. The provisioning user in keycloak is meet/meet.
|
||||
You can use Visio on https://meet.127.0.0.1.nip.io from the local device. The provisioning user in keycloak is meet/meet.
|
||||
|
||||
## All options
|
||||
|
||||
@@ -240,92 +251,110 @@ These are the environmental options available on meet backend.
|
||||
|
||||
| Option | Description | default |
|
||||
| ----------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| DJANGO_ALLOWED_HOSTS | Host that are allowed | [] |
|
||||
| DJANGO_SECRET_KEY | Secret key used | |
|
||||
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence system checks | [] |
|
||||
| DJANGO_ALLOW_UNSECURE_USER_LISTING | | false |
|
||||
| DATA_DIR | Data directory location | /data |
|
||||
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
|
||||
| DJANGO_SECRET_KEY | Secret key used for Django security | |
|
||||
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
|
||||
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
|
||||
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_NAME | name of the database | meet |
|
||||
| DB_USER | user used to connect to database | dinum |
|
||||
| DB_PASSWORD | password used to connect to the database | pass |
|
||||
| DB_HOST | hostname of the database | localhost |
|
||||
| DB_PORT | port to connect to database | 5432 |
|
||||
| DB_NAME | Name of the database | meet |
|
||||
| DB_USER | User used to connect to database | dinum |
|
||||
| DB_PASSWORD | Password used to connect to the database | pass |
|
||||
| DB_HOST | Hostname of the database | localhost |
|
||||
| DB_PORT | Port to connect to database | 5432 |
|
||||
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
|
||||
| AWS_S3_ACCESS_KEY_ID | s3 access key | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | s3 secret key | |
|
||||
| AWS_S3_REGION_NAME | s3 region | |
|
||||
| AWS_STORAGE_BUCKET_NAME | s3 bucket name | |
|
||||
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
|
||||
| AWS_S3_REGION_NAME | S3 region | |
|
||||
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
|
||||
| DJANGO_LANGUAGE_CODE | Default language | en-us |
|
||||
| REDIS_URL | redis endpoint | redis://redis:6379/1 |
|
||||
| REQUEST_ENTRY_THROTTLE_RATES | throttle rates | 150/minute |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | deploy check | False |
|
||||
| FRONTEND_ANALYTICS | analytics information | {} |
|
||||
| FRONTEND_SUPPORT | crisp frontend support | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | silence livekit debug | false |
|
||||
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_HOST | host of the email server | |
|
||||
| DJANGO_EMAIL_HOST_USER | user to connect to the email server | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | password to connect tto the email server | |
|
||||
| DJANGO_EMAIL_PORT | por tot connect to the email server | |
|
||||
| DJANGO_EMAIL_USE_TLS | enable tls on email connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | enable tls on email connection | false |
|
||||
| DJANGO_EMAIL_FROM | email from account | from@example.com |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all cors origins | true |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | origins to allow in string | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | origins to allow in regex | [] |
|
||||
| SENTRY_DSN | sentry server | |
|
||||
| DJANGO_CELERY_BROKER_URL | celery broker host | redis://redis:6379/0 |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker options | {} |
|
||||
| OIDC_CREATE_USER | create oidc user if not exists | false |
|
||||
| OIDC_VERIFY_SSL | verify ssl for oidc | true |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | fallback to email for identification | false |
|
||||
| OIDC_RP_SIGN_ALGO | token verification algoritm used by oidc | RS256 |
|
||||
| OIDC_RP_CLIENT_ID | oidc client | meet |
|
||||
| OIDC_RP_CLIENT_SECRET | oidc client secret | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | oidc endpoint for JWKS | |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | oidc endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | oidc endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | oidc endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | oidc endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | oidc endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | extra parameters for oidc request | |
|
||||
| OIDC_RP_SCOPES | oidc scopes | openid email |
|
||||
| LOGIN_REDIRECT_URL | login redirect url | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | login redurect url for failure | |
|
||||
| LOGOUT_REDIRECT_URL | url to redirect to on logout | |
|
||||
| OIDC_USE_NONCE | use nonce for oidc | true |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | require https for oidc | false |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | allowed redirect hosts for oidc | [] |
|
||||
| OIDC_STORE_ID_TOKEN | store oidc ID token | true |
|
||||
| ALLOW_LOGOUT_GET_METHOD | allow logout through get method | true |
|
||||
| OIDC_REDIRECT_FIELD_NAME | direct field for oidc | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | shortname claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | required claims from OIDC token | [] |
|
||||
| LIVEKIT_API_KEY | livekit api key | |
|
||||
| LIVEKIT_API_SECRET | livekit api secret | |
|
||||
| LIVEKIT_API_URL | livekit api url | |
|
||||
| RESOURCE_DEFAULT_ACCESS_LEVEL | default resource access level for rooms | public |
|
||||
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
|
||||
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
||||
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
||||
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
||||
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
|
||||
| FRONTEND_ANALYTICS | Analytics information | {} |
|
||||
| FRONTEND_SUPPORT | Crisp frontend support configuration | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
|
||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_HOST | Host of the email server | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
|
||||
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
|
||||
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
|
||||
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
|
||||
| EMAIL_BRAND_NAME | Email branding name | |
|
||||
| EMAIL_SUPPORT_EMAIL | Support email address | |
|
||||
| EMAIL_LOGO_IMG | Email logo image | |
|
||||
| EMAIL_DOMAIN | Email domain | |
|
||||
| EMAIL_APP_BASE_URL | Email app base URL | |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | true |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
|
||||
| SENTRY_DSN | Sentry server DSN | |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
|
||||
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
|
||||
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
|
||||
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
|
||||
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
|
||||
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
|
||||
| OIDC_RP_SCOPES | OIDC scopes | openid email |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
|
||||
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
|
||||
| LOGIN_REDIRECT_URL | Login redirect URL | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
|
||||
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
|
||||
| LIVEKIT_API_KEY | LiveKit API key | |
|
||||
| LIVEKIT_API_SECRET | LiveKit API secret | |
|
||||
| LIVEKIT_API_URL | LiveKit API URL | |
|
||||
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
|
||||
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
|
||||
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
|
||||
| RECORDING_ENABLE | record meeting option | false |
|
||||
| RECORDING_OUTPUT_FOLDER | folder to store meetings | recordings |
|
||||
| RECORDING_WORKER_CLASSES | worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
|
||||
| RECORDING_EVENT_PARSER_CLASS | storage event engine for recording | core.recording.event.parsers.MinioParser |
|
||||
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | enable storage event authorization | true |
|
||||
| RECORDING_STORAGE_EVENT_ENABLE | enable recording storage events | false |
|
||||
| RECORDING_STORAGE_EVENT_TOKEN | recording storage event token | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | api token voor summary service | |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | markering class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | breva api key for marketing emails | |
|
||||
| BREVO_API_CONTACT_LIST_IDS | brevo api contact list ids | [] |
|
||||
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | brevo contact attributes | {"VISIO_USER": True} |
|
||||
| BREVO_API_TIMEOUT | brevo timeout | 1 |
|
||||
| LOBBY_KEY_PREFIX | lobby prefix | room_lobby |
|
||||
| LOBBY_WAITING_TIMEOUT | lobby waiting tumeout | 3 |
|
||||
| LOBBY_DENIED_TIMEOUT | lobby deny timeout | 5 |
|
||||
| LOBBY_ACCEPTED_TIMEOUT | lobby accept timeout | 21600 |
|
||||
| LOBBY_NOTIFICATION_TYPE | lobby notification types | participantWaiting |
|
||||
| LOBBY_COOKIE_NAME | lobby cookie name | lobbyParticipantId |
|
||||
| RECORDING_ENABLE | Record meeting option | false |
|
||||
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
|
||||
| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
|
||||
| RECORDING_EVENT_PARSER_CLASS | Storage event engine for recording | core.recording.event.parsers.MinioParser |
|
||||
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | Enable storage event authorization | true |
|
||||
| RECORDING_STORAGE_EVENT_ENABLE | Enable recording storage events | false |
|
||||
| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | |
|
||||
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
|
||||
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | Brevo API key for marketing emails | |
|
||||
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
|
||||
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
|
||||
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
|
||||
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
|
||||
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
|
||||
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
|
||||
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
|
||||
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
|
||||
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
|
||||
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
|
||||
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
|
||||
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
|
||||
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
|
||||
|
||||
@@ -54,3 +54,6 @@ ALLOW_UNREGISTERED_ROOMS=False
|
||||
|
||||
# Recording
|
||||
SCREEN_RECORDING_BASE_URL=http://localhost:3000/recordings
|
||||
|
||||
# Telephony
|
||||
ROOM_TELEPHONY_ENABLED=True
|
||||
|
||||
@@ -99,6 +99,7 @@ class ResourceAccessInline(admin.TabularInline):
|
||||
|
||||
model = models.ResourceAccess
|
||||
extra = 0
|
||||
autocomplete_fields = ["user"]
|
||||
|
||||
|
||||
@admin.register(models.Room)
|
||||
@@ -106,6 +107,10 @@ class RoomAdmin(admin.ModelAdmin):
|
||||
"""Room admin interface declaration."""
|
||||
|
||||
inlines = (ResourceAccessInline,)
|
||||
search_fields = ["name", "slug", "=id"]
|
||||
list_display = ["name", "slug", "access_level", "created_at"]
|
||||
list_filter = ["access_level", "created_at"]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class RecordingAccessInline(admin.TabularInline):
|
||||
@@ -120,4 +125,28 @@ class RecordingAdmin(admin.ModelAdmin):
|
||||
"""Recording admin interface declaration."""
|
||||
|
||||
inlines = (RecordingAccessInline,)
|
||||
list_display = ("id", "status", "room", "created_at", "worker_id")
|
||||
search_fields = ["status", "=id", "worker_id", "room__slug", "=room__id"]
|
||||
list_display = ("id", "status", "room", "get_owner", "created_at", "worker_id")
|
||||
list_filter = ["status", "room", "created_at"]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
|
||||
return super().get_queryset(request).prefetch_related("accesses__user")
|
||||
|
||||
def get_owner(self, obj):
|
||||
"""Return the owner of the recording for display in the admin list."""
|
||||
|
||||
owners = [
|
||||
access
|
||||
for access in obj.accesses.all()
|
||||
if access.role == models.RoleChoices.OWNER
|
||||
]
|
||||
|
||||
if not owners:
|
||||
return _("No owner")
|
||||
|
||||
if len(owners) > 1:
|
||||
return _("Multiple owners")
|
||||
|
||||
return str(owners[0].user)
|
||||
|
||||
@@ -37,7 +37,7 @@ class ResourceAccessSerializerMixin:
|
||||
# Update
|
||||
self.instance
|
||||
and (
|
||||
data["role"] == models.RoleChoices.OWNER
|
||||
data.get("role") == models.RoleChoices.OWNER
|
||||
and not self.instance.resource.is_owner(user)
|
||||
or self.instance.role == models.RoleChoices.OWNER
|
||||
and not self.instance.user == user
|
||||
|
||||
@@ -93,6 +93,6 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
|
||||
pass
|
||||
except User.MultipleObjectsReturned as e:
|
||||
raise SuspiciousOperation(
|
||||
_("Multiple user accounts share a common email.")
|
||||
"Multiple user accounts share a common email."
|
||||
) from e
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-13 08:22
|
||||
|
||||
import secrets
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def generate_pin_for_rooms(apps, schema_editor):
|
||||
"""Generate unique 10-digit PIN codes for existing rooms.
|
||||
The PIN code is required for SIP telephony features.
|
||||
"""
|
||||
Room = apps.get_model('core', 'Room')
|
||||
rooms_without_pin_code = Room.objects.filter(pin_code__isnull=True)
|
||||
existing_pins = set(Room.objects.values_list('pin_code', flat=True))
|
||||
|
||||
length = 10
|
||||
|
||||
def generate_pin_code():
|
||||
while True:
|
||||
pin_code = str(secrets.randbelow(10 ** length)).zfill(length)
|
||||
if not pin_code in existing_pins:
|
||||
return pin_code
|
||||
|
||||
for room in rooms_without_pin_code:
|
||||
room.pin_code = generate_pin_code()
|
||||
room.save()
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_alter_user_language'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='room',
|
||||
name='pin_code',
|
||||
field=models.CharField(blank=True, help_text='Unique n-digit code that identifies this room in telephony mode.', max_length=None, null=True, unique=True, verbose_name='Room PIN code'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
generate_pin_for_rooms,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,7 @@
|
||||
Declare and configure the models for the Meet core application
|
||||
"""
|
||||
|
||||
import secrets
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from logging import getLogger
|
||||
@@ -14,7 +15,6 @@ from django.core import mail, validators
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.text import capfirst, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -164,7 +164,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
)
|
||||
language = models.CharField(
|
||||
max_length=10,
|
||||
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
|
||||
choices=settings.LANGUAGES,
|
||||
default=settings.LANGUAGE_CODE,
|
||||
verbose_name=_("language"),
|
||||
help_text=_("The language in which the user wants to see the interface."),
|
||||
@@ -383,6 +383,14 @@ class Room(Resource):
|
||||
verbose_name=_("Visio room configuration"),
|
||||
help_text=_("Values for Visio parameters to configure the room."),
|
||||
)
|
||||
pin_code = models.CharField(
|
||||
max_length=None,
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Room PIN code"),
|
||||
help_text=_("Unique n-digit code that identifies this room in telephony mode."),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "meet_room"
|
||||
@@ -393,6 +401,14 @@ class Room(Resource):
|
||||
def __str__(self):
|
||||
return capfirst(self.name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Generate a unique n-digit pin code for new rooms."""
|
||||
if settings.ROOM_TELEPHONY_ENABLED and not self.pk and not self.pin_code:
|
||||
self.pin_code = self.generate_unique_pin_code(
|
||||
length=settings.ROOM_TELEPHONY_PIN_LENGTH
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean_fields(self, exclude=None):
|
||||
"""
|
||||
Automatically generate the slug from the name and make sure it does not look like a UUID.
|
||||
@@ -407,6 +423,7 @@ class Room(Resource):
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({"name": f'Room name "{self.name:s}" is reserved.'})
|
||||
|
||||
super().clean_fields(exclude=exclude)
|
||||
|
||||
@property
|
||||
@@ -414,6 +431,31 @@ class Room(Resource):
|
||||
"""Check if a room is public"""
|
||||
return self.access_level == RoomAccessLevel.PUBLIC
|
||||
|
||||
@staticmethod
|
||||
def generate_unique_pin_code(length):
|
||||
"""Generate a unique n-digit PIN code"""
|
||||
|
||||
if length < 4:
|
||||
raise ValueError(
|
||||
"PIN code length must be at least 4 digits for minimal security"
|
||||
)
|
||||
|
||||
max_value = 10**length
|
||||
|
||||
for _ in range(settings.ROOM_TELEPHONY_PIN_MAX_RETRIES):
|
||||
pin_code = str(secrets.randbelow(max_value)).zfill(length)
|
||||
if not Room.objects.filter(pin_code=pin_code).exists():
|
||||
return pin_code
|
||||
|
||||
# Log a warning as a temporary measure until backend observability is implemented.
|
||||
logger.warning(
|
||||
"Failed to generate unique PIN code of length %s after %s attempts",
|
||||
length,
|
||||
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class BaseAccessManager(models.Manager):
|
||||
"""Base manager for handling resource access control."""
|
||||
|
||||
@@ -55,7 +55,7 @@ class StorageEventAuthentication(BaseAuthentication):
|
||||
if not required_token:
|
||||
if settings.RECORDING_ENABLE_STORAGE_EVENT_AUTH:
|
||||
raise AuthenticationFailed(
|
||||
_("Authentication is enabled but token is not configured.")
|
||||
"Authentication is enabled but token is not configured."
|
||||
)
|
||||
|
||||
return MachineUser(), None
|
||||
@@ -67,7 +67,7 @@ class StorageEventAuthentication(BaseAuthentication):
|
||||
"Authentication failed: Missing Authorization header (ip: %s)",
|
||||
request.META.get("REMOTE_ADDR"),
|
||||
)
|
||||
raise AuthenticationFailed(_("Authorization header is required"))
|
||||
raise AuthenticationFailed("Authorization header is required")
|
||||
|
||||
auth_parts = auth_header.split(" ")
|
||||
if len(auth_parts) != 2 or auth_parts[0] != self.TOKEN_TYPE:
|
||||
@@ -75,7 +75,7 @@ class StorageEventAuthentication(BaseAuthentication):
|
||||
"Authentication failed: Invalid authorization header (ip: %s)",
|
||||
request.META.get("REMOTE_ADDR"),
|
||||
)
|
||||
raise AuthenticationFailed(_("Invalid authorization header."))
|
||||
raise AuthenticationFailed("Invalid authorization header.")
|
||||
|
||||
token = auth_parts[1]
|
||||
|
||||
@@ -85,7 +85,7 @@ class StorageEventAuthentication(BaseAuthentication):
|
||||
"Authentication failed: Invalid token (ip: %s)",
|
||||
request.META.get("REMOTE_ADDR"),
|
||||
)
|
||||
raise AuthenticationFailed(_("Invalid token"))
|
||||
raise AuthenticationFailed("Invalid token")
|
||||
|
||||
return MachineUser(), token
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
import brevo_python
|
||||
import urllib3
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -120,8 +121,11 @@ class BrevoMarketingService:
|
||||
|
||||
try:
|
||||
response = contact_api.create_contact(contact, **api_configurations)
|
||||
except brevo_python.rest.ApiException as err:
|
||||
logger.exception("Failed to create contact in Brevo")
|
||||
except (
|
||||
brevo_python.rest.ApiException,
|
||||
urllib3.exceptions.ReadTimeoutError,
|
||||
) as err:
|
||||
logger.warning("Failed to create contact in Brevo", exc_info=True)
|
||||
raise ContactCreationError("Failed to create contact in Brevo") from err
|
||||
|
||||
return response
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
import brevo_python
|
||||
import pytest
|
||||
import urllib3
|
||||
|
||||
from core.services.marketing import (
|
||||
BrevoMarketingService,
|
||||
@@ -133,6 +134,34 @@ def test_create_contact_api_error(mock_contact_api):
|
||||
brevo_service.create_contact(valid_contact_data)
|
||||
|
||||
|
||||
@mock.patch("brevo_python.ContactsApi")
|
||||
def test_create_contact_timeout_error(mock_contact_api):
|
||||
"""Test contact creation timeout error handling."""
|
||||
|
||||
mock_api = mock_contact_api.return_value
|
||||
settings.BREVO_API_KEY = "test-api-key"
|
||||
settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4]
|
||||
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
|
||||
|
||||
valid_contact_data = ContactData(
|
||||
email="test@example.com",
|
||||
attributes={"first_name": "Test"},
|
||||
list_ids=[1, 2],
|
||||
update_enabled=True,
|
||||
)
|
||||
|
||||
brevo_service = BrevoMarketingService()
|
||||
|
||||
mock_api.create_contact.side_effect = urllib3.exceptions.ReadTimeoutError(
|
||||
pool=mock.Mock(),
|
||||
url="https://api.brevo.com/v3/endpoint",
|
||||
message="HTTPSConnectionPool(host='api.brevo.com', port=443): Read timed out.",
|
||||
)
|
||||
|
||||
with pytest.raises(ContactCreationError, match="Failed to create contact in Brevo"):
|
||||
brevo_service.create_contact(valid_contact_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_marketing_cache():
|
||||
"""Clear marketing service cache between tests."""
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
Unit tests for the Room model
|
||||
"""
|
||||
|
||||
# pylint: disable=W0613
|
||||
|
||||
import secrets
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -175,3 +181,169 @@ def test_models_rooms_is_public_property():
|
||||
# Test non-public room
|
||||
private_room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
assert private_room.is_public is False
|
||||
|
||||
|
||||
@mock.patch.object(Room, "generate_unique_pin_code")
|
||||
def test_telephony_disabled_skips_pin_generation(
|
||||
mock_generate_unique_pin_code, settings
|
||||
):
|
||||
"""Telephony disabled should not generate pin codes."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = False
|
||||
|
||||
room = RoomFactory()
|
||||
|
||||
mock_generate_unique_pin_code.assert_not_called()
|
||||
assert room.pin_code is None
|
||||
|
||||
|
||||
def test_default_and_custom_pin_length(settings):
|
||||
"""Pin codes should be created with correct configured length."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
|
||||
room = RoomFactory()
|
||||
|
||||
# Assert default value is 10 for collision reasons
|
||||
assert len(room.pin_code) == 10
|
||||
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
|
||||
|
||||
room = RoomFactory()
|
||||
|
||||
# Assert custom size
|
||||
assert len(room.pin_code) == 5
|
||||
|
||||
|
||||
def test_room_updates_preserve_pin_code(settings):
|
||||
"""Room updates should preserve existing pin code."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
|
||||
room = RoomFactory()
|
||||
|
||||
# Store the original pin code to compare after updates
|
||||
previous_pin_code = room.pin_code
|
||||
|
||||
# If this method is called, it would indicate the pin is being regenerated unnecessarily
|
||||
with mock.patch.object(
|
||||
Room, "generate_unique_pin_code"
|
||||
) as mock_generate_unique_pin_code:
|
||||
# Explicitly call save to persist the changes to the room
|
||||
room.slug = "aaa-aaaa-aaa"
|
||||
room.save()
|
||||
assert room.pin_code == previous_pin_code
|
||||
mock_generate_unique_pin_code.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_telephony_enabled", [True, False])
|
||||
def test_manual_pin_code_updates(is_telephony_enabled, settings):
|
||||
"""Manual pin code changes should persist regardless of telephony setting."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = is_telephony_enabled
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
|
||||
|
||||
room = RoomFactory()
|
||||
assert room.pin_code != "12345"
|
||||
|
||||
room.pin_code = "12345"
|
||||
room.save()
|
||||
|
||||
assert room.pin_code == "12345"
|
||||
|
||||
# No data validation when manual updates are made
|
||||
room.pin_code = "123"
|
||||
room.save()
|
||||
|
||||
assert room.pin_code == "123"
|
||||
|
||||
|
||||
def test_pin_code_uniqueness(settings):
|
||||
"""Duplicate pin codes should raise validation error."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
|
||||
|
||||
RoomFactory(pin_code="12345")
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
RoomFactory(pin_code="12345")
|
||||
|
||||
assert "Room with this Room PIN code already exists." in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid_length", [0, 1, 2, 3])
|
||||
def test_pin_code_minimum_length(invalid_length, settings):
|
||||
"""Pin codes should enforce minimum length for security."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = 4
|
||||
|
||||
# Assert no exception is raised with a valid length
|
||||
RoomFactory()
|
||||
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = invalid_length
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
RoomFactory()
|
||||
|
||||
assert "PIN code length must be at least 4 digits for minimal security" in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(Logger, "warning")
|
||||
@mock.patch.object(secrets, "randbelow", return_value=12345)
|
||||
def test_pin_generation_max_retries(mock_randbelow, mock_logger, settings):
|
||||
"""Pin generation should give up after max retries."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
|
||||
|
||||
RoomFactory(pin_code="12345")
|
||||
|
||||
# Assert default max retries is low, 5
|
||||
room1 = RoomFactory()
|
||||
assert mock_randbelow.call_count == 5
|
||||
assert room1.pin_code is None
|
||||
|
||||
mock_logger.assert_called_once_with(
|
||||
"Failed to generate unique PIN code of length %s after %s attempts", 5, 5
|
||||
)
|
||||
|
||||
mock_logger.reset_mock()
|
||||
mock_randbelow.reset_mock()
|
||||
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES = 3
|
||||
|
||||
room2 = RoomFactory()
|
||||
assert mock_randbelow.call_count == 3
|
||||
assert room2.pin_code is None
|
||||
|
||||
mock_logger.assert_called_once_with(
|
||||
"Failed to generate unique PIN code of length %s after %s attempts", 5, 3
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(secrets, "randbelow", return_value=12345)
|
||||
def test_pin_code_zero_padding(mock_randbelow, settings):
|
||||
"""Pin codes should be zero-padded to meet required length."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PIN_LENGTH = 10
|
||||
|
||||
room = RoomFactory()
|
||||
assert room.pin_code == "0000012345"
|
||||
|
||||
|
||||
@mock.patch.object(secrets, "randbelow", return_value=12345)
|
||||
def test_pin_generation_upper_bound(mock_randbelow, settings):
|
||||
"""Random number generator should use correct upper bound based on pin length."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = False
|
||||
|
||||
room = RoomFactory()
|
||||
|
||||
room.generate_unique_pin_code(length=5)
|
||||
|
||||
# Assert called with the right exclusive upper bound, 10^5
|
||||
mock_randbelow.assert_called_with(100000)
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:26
|
||||
msgid "Personal info"
|
||||
msgstr "Persönliche Informationen"
|
||||
|
||||
#: core/admin.py:39
|
||||
msgid "Permissions"
|
||||
msgstr "Berechtigungen"
|
||||
|
||||
#: core/admin.py:51
|
||||
msgid "Important dates"
|
||||
msgstr "Wichtige Daten"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "No owner"
|
||||
msgstr "Kein Eigentümer"
|
||||
|
||||
#: core/admin.py:146
|
||||
msgid "Multiple owners"
|
||||
msgstr "Mehrere Eigentümer"
|
||||
|
||||
#: core/api/serializers.py:63
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
||||
"hinzuzufügen."
|
||||
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Eigentümer"
|
||||
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Gestartet"
|
||||
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Beendet"
|
||||
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Gespeichert"
|
||||
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Abgebrochen"
|
||||
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Start fehlgeschlagen"
|
||||
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stopp fehlgeschlagen"
|
||||
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Benachrichtigung erfolgreich"
|
||||
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
||||
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSKRIPT"
|
||||
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Öffentlicher Zugriff"
|
||||
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrauenswürdiger Zugriff"
|
||||
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Eingeschränkter Zugriff"
|
||||
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "ID"
|
||||
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Primärschlüssel des Eintrags als UUID"
|
||||
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "erstellt am"
|
||||
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
||||
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "aktualisiert am"
|
||||
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
||||
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
msgstr ""
|
||||
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
|
||||
"und die Zeichen @/./+/-/_ enthalten."
|
||||
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "Sub"
|
||||
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Erforderlich. Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ sind "
|
||||
"erlaubt."
|
||||
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "Administrator-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "Vollständiger Name"
|
||||
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "Kurzname"
|
||||
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
||||
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
||||
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
||||
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "Mitarbeiterstatus"
|
||||
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
||||
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
msgstr ""
|
||||
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
||||
"anstelle des Löschens des Kontos."
|
||||
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Ressourcen"
|
||||
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Ressourcenzugriff"
|
||||
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Ressourcenzugriffe"
|
||||
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
||||
"bereits."
|
||||
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-Raumkonfiguration"
|
||||
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
||||
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "PIN-Code für den Raum"
|
||||
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Raum"
|
||||
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Räume"
|
||||
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker-ID"
|
||||
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
msgstr ""
|
||||
"Geben Sie eine ID für die Aufzeichnung des Workers ein. Diese ID bleibt "
|
||||
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
||||
"ermöglicht."
|
||||
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Aufzeichnungsmodus"
|
||||
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
||||
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Aufzeichnung"
|
||||
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Aufzeichnungen"
|
||||
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Beziehung Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Ihre Aufzeichnung ist bereit"
|
||||
|
||||
#: core/services/invitation.py:44
|
||||
#, python-brace-format
|
||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||
msgstr "Videoanruf läuft: {sender.email} wartet auf Ihre Teilnahme"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:159
|
||||
#: core/templates/mail/html/screen_recording.html:159
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/text/screen_recording.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo-E-Mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:189
|
||||
#: core/templates/mail/text/invitation.txt:5
|
||||
msgid "invites you to join an ongoing video call"
|
||||
msgstr "lädt Sie zu einem laufenden Videoanruf ein"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:200
|
||||
#: core/templates/mail/text/invitation.txt:7
|
||||
msgid "JOIN THE CALL"
|
||||
msgstr "AM ANRUF TEILNEHMEN"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:227
|
||||
#: core/templates/mail/text/invitation.txt:13
|
||||
msgid ""
|
||||
"If you can't click the button, copy and paste the URL into your browser to "
|
||||
"join the call."
|
||||
msgstr ""
|
||||
"Wenn Sie den Button nicht anklicken können, kopieren Sie die URL und fügen "
|
||||
"Sie sie in Ihren Browser ein, um am Anruf teilzunehmen."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:235
|
||||
#: core/templates/mail/text/invitation.txt:15
|
||||
msgid "Tips for a better experience:"
|
||||
msgstr "Tipps für ein besseres Erlebnis:"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:237
|
||||
#: core/templates/mail/text/invitation.txt:17
|
||||
msgid "Use Chrome or Firefox for better call quality"
|
||||
msgstr "Verwenden Sie Chrome oder Firefox für eine bessere Anrufqualität"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:238
|
||||
#: core/templates/mail/text/invitation.txt:18
|
||||
msgid "Test your microphone and camera before joining"
|
||||
msgstr "Testen Sie Ihr Mikrofon und Ihre Kamera vor dem Beitritt"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:239
|
||||
#: core/templates/mail/text/invitation.txt:19
|
||||
msgid "Make sure you have a stable internet connection"
|
||||
msgstr "Stellen Sie sicher, dass Sie eine stabile Internetverbindung haben"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:248
|
||||
#: core/templates/mail/html/screen_recording.html:240
|
||||
#: core/templates/mail/text/invitation.txt:21
|
||||
#: core/templates/mail/text/screen_recording.txt:22
|
||||
#, python-format
|
||||
msgid " Thank you for using %(brandname)s. "
|
||||
msgstr " Vielen Dank für die Nutzung von %(brandname)s. "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:188
|
||||
#: core/templates/mail/text/screen_recording.txt:6
|
||||
msgid "Your recording is ready!"
|
||||
msgstr "Ihre Aufzeichnung ist fertig!"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:195
|
||||
#: core/templates/mail/text/screen_recording.txt:8
|
||||
#, python-format
|
||||
msgid ""
|
||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||
"%(recording_time)s is now ready to download. "
|
||||
msgstr ""
|
||||
" Ihre Aufzeichnung von \"%(room_name)s\" am %(recording_date)s um "
|
||||
"%(recording_time)s steht nun zum Herunterladen bereit. "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:195
|
||||
#: core/templates/mail/text/screen_recording.txt:8
|
||||
#, python-format
|
||||
msgid " The recording will expire in %(days)s days. "
|
||||
msgstr " Die Aufzeichnung wird in %(days)s Tagen ablaufen. "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:201
|
||||
#: core/templates/mail/text/screen_recording.txt:10
|
||||
msgid "To keep this recording permanently:"
|
||||
msgstr "So speichern Sie diese Aufzeichnung dauerhaft:"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:203
|
||||
#: core/templates/mail/text/screen_recording.txt:12
|
||||
msgid "Click the \"Open\" button below "
|
||||
msgstr "Klicken Sie auf den Button „Öffnen“ unten "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:204
|
||||
#: core/templates/mail/text/screen_recording.txt:13
|
||||
msgid "Use the \"Download\" button in the interface "
|
||||
msgstr "Verwenden Sie den Button „Herunterladen“ in der Oberfläche "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:205
|
||||
#: core/templates/mail/text/screen_recording.txt:14
|
||||
msgid "Save the file to your preferred location"
|
||||
msgstr "Speichern Sie die Datei an einem gewünschten Ort"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:216
|
||||
#: core/templates/mail/text/screen_recording.txt:16
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:225
|
||||
#: core/templates/mail/text/screen_recording.txt:18
|
||||
#, python-format
|
||||
msgid ""
|
||||
" If you have any questions or need assistance, please contact our support "
|
||||
"team at %(support_email)s. "
|
||||
msgstr ""
|
||||
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
|
||||
"an unser Support-Team unter %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:162
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: meet/settings.py:163
|
||||
msgid "French"
|
||||
msgstr "Französisch"
|
||||
|
||||
#: meet/settings.py:164
|
||||
msgid "Dutch"
|
||||
msgstr "Niederländisch"
|
||||
|
||||
#: meet/settings.py:165
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-23 14:04+0000\n"
|
||||
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,111 +29,107 @@ msgstr "Permissions"
|
||||
msgid "Important dates"
|
||||
msgstr "Important dates"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "No owner"
|
||||
msgstr "No owner"
|
||||
|
||||
#: core/admin.py:146
|
||||
msgid "Multiple owners"
|
||||
msgstr "Multiple owners"
|
||||
|
||||
#: core/api/serializers.py:63
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr "You must be administrator or owner of a room to add accesses to it."
|
||||
|
||||
#: core/authentication/backends.py:77
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr "User info contained no recognizable user identification"
|
||||
|
||||
#: core/authentication/backends.py:102
|
||||
msgid "User account is disabled"
|
||||
msgstr "User account is disabled"
|
||||
|
||||
#: core/authentication/backends.py:142
|
||||
msgid "Multiple user accounts share a common email."
|
||||
msgstr "Multiple user accounts share a common email."
|
||||
|
||||
#: core/models.py:30
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Member"
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#: core/models.py:48
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Initiated"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Active"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Stopped"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Saved"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Aborted"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Failed to Start"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Failed to Stop"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification succeeded"
|
||||
|
||||
#: core/models.py:82
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCREEN_RECORDING"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Public Access"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Trusted Access"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Restricted Access"
|
||||
|
||||
#: core/models.py:103
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primary key for the record as UUID"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "created on"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date and time at which a record was created"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "updated on"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date and time at which a record was last updated"
|
||||
|
||||
#: core/models.py:137
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -141,11 +137,11 @@ msgstr ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
|
||||
#: core/models.py:143
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:145
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
@@ -153,55 +149,55 @@ msgstr ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
|
||||
#: core/models.py:153
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "identity email address"
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "admin email address"
|
||||
|
||||
#: core/models.py:160
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "full name"
|
||||
|
||||
#: core/models.py:162
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "short name"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "language"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "The language in which the user wants to see the interface."
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "The timezone in which the user wants to see times."
|
||||
|
||||
#: core/models.py:178
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "device"
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Whether the user is a device or a real user."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "staff status"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Whether the user can log into this admin site."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "active"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -209,55 +205,63 @@ msgstr ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
|
||||
#: core/models.py:204
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "user"
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "users"
|
||||
|
||||
#: core/models.py:264
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Resource"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Resources"
|
||||
|
||||
#: core/models.py:319
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Resource access"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Resource accesses"
|
||||
|
||||
#: core/models.py:326
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Resource access with this User and Resource already exists."
|
||||
|
||||
#: core/models.py:382
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio room configuration"
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Values for Visio parameters to configure the room."
|
||||
|
||||
#: core/models.py:389 core/models.py:509
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "Room PIN code"
|
||||
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Room"
|
||||
|
||||
#: core/models.py:390
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Rooms"
|
||||
|
||||
#: core/models.py:520
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:522
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -265,58 +269,42 @@ msgstr ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
|
||||
#: core/models.py:530
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Recording mode"
|
||||
|
||||
#: core/models.py:531
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Defines the mode of recording being called."
|
||||
|
||||
#: core/models.py:537
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Recording"
|
||||
|
||||
#: core/models.py:538
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Recordings"
|
||||
|
||||
#: core/models.py:646
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Recording/user relation"
|
||||
|
||||
#: core/models.py:647
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Recording/user relations"
|
||||
|
||||
#: core/models.py:653
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "This user is already in this recording."
|
||||
|
||||
#: core/models.py:659
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "This team is already in this recording."
|
||||
|
||||
#: core/models.py:665
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Either user or team must be set, not both."
|
||||
|
||||
#: core/recording/event/authentication.py:58
|
||||
msgid "Authentication is enabled but token is not configured."
|
||||
msgstr "Authentication is enabled but token is not configured."
|
||||
|
||||
#: core/recording/event/authentication.py:70
|
||||
msgid "Authorization header is required"
|
||||
msgstr "Authorization header is required"
|
||||
|
||||
#: core/recording/event/authentication.py:78
|
||||
msgid "Invalid authorization header."
|
||||
msgstr "Invalid authorization header."
|
||||
|
||||
#: core/recording/event/authentication.py:88
|
||||
msgid "Invalid token"
|
||||
msgstr "Invalid token"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Your recording is ready"
|
||||
@@ -447,3 +435,7 @@ msgstr "French"
|
||||
#: meet/settings.py:164
|
||||
msgid "Dutch"
|
||||
msgstr "Dutch"
|
||||
|
||||
#: meet/settings.py:165
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-23 14:04+0000\n"
|
||||
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,129 +29,123 @@ msgstr "Permissions"
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "No owner"
|
||||
msgstr "Pas de propriétaire"
|
||||
|
||||
#: core/admin.py:146
|
||||
msgid "Multiple owners"
|
||||
msgstr "Plusieurs propriétaires"
|
||||
|
||||
#: core/api/serializers.py:63
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
||||
"des accès."
|
||||
|
||||
#: core/authentication/backends.py:77
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr ""
|
||||
"Les informations utilisateur ne contiennent aucune identification "
|
||||
"utilisateur reconnaissable"
|
||||
|
||||
#: core/authentication/backends.py:102
|
||||
msgid "User account is disabled"
|
||||
msgstr "Le compte utilisateur est désactivé"
|
||||
|
||||
#: core/authentication/backends.py:142
|
||||
msgid "Multiple user accounts share a common email."
|
||||
msgstr "Plusieurs comptes utilisateur partagent une adresse e-mail commune."
|
||||
|
||||
#: core/models.py:30
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:48
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Initié"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Arrêté"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Enregistré"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Abandonné"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Échec au démarrage"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Échec à l'arrêt"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification réussie"
|
||||
|
||||
#: core/models.py:82
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "ENREGISTREMENT_ÉCRAN"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPTION"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Accès public"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Accès de confiance"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Accès restreint"
|
||||
|
||||
#: core/models.py:103
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "créé le"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure auxquelles un enregistrement a été créé"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
||||
"fois"
|
||||
|
||||
#: core/models.py:137
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
msgstr ""
|
||||
"Entrez un identifiant valide. Cette valeur ne peut contenir que des lettres, "
|
||||
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, "
|
||||
"des chiffres et les caractères @/./+/-/_."
|
||||
|
||||
#: core/models.py:143
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "identifiant"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:145
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
@@ -159,55 +153,55 @@ msgstr ""
|
||||
"Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./"
|
||||
"+/-/_ uniquement."
|
||||
|
||||
#: core/models.py:153
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail d'administrateur"
|
||||
|
||||
#: core/models.py:160
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:162
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:178
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "statut du personnel"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -215,116 +209,108 @@ msgstr ""
|
||||
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
||||
"au lieu de supprimer des comptes."
|
||||
|
||||
#: core/models.py:204
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:264
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Ressources"
|
||||
|
||||
#: core/models.py:319
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:326
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
||||
|
||||
#: core/models.py:382
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Configuration de la salle de visioconférence"
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
||||
|
||||
#: core/models.py:389 core/models.py:509
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "Code PIN de la salle"
|
||||
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Salle"
|
||||
|
||||
#: core/models.py:390
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Salles"
|
||||
|
||||
#: core/models.py:520
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "ID du Worker"
|
||||
|
||||
#: core/models.py:522
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
msgstr ""
|
||||
"Entrez un identifiant pour l'enregistrement du travailleur. Cet identifiant "
|
||||
"est conservé même lorsque le travailleur s'arrête, permettant un suivi "
|
||||
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant "
|
||||
"est conservé même lorsque le Worker s'arrête, permettant un suivi "
|
||||
"facile."
|
||||
|
||||
#: core/models.py:530
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Mode d'enregistrement"
|
||||
|
||||
#: core/models.py:531
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Définit le mode d'enregistrement appelé."
|
||||
|
||||
#: core/models.py:537
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Enregistrement"
|
||||
|
||||
#: core/models.py:538
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Enregistrements"
|
||||
|
||||
#: core/models.py:646
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Relation enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:647
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Relations enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:653
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:659
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Cette équipe est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:665
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
||||
|
||||
#: core/recording/event/authentication.py:58
|
||||
msgid "Authentication is enabled but token is not configured."
|
||||
msgstr "L'authentification est activée mais le jeton n'est pas configuré."
|
||||
|
||||
#: core/recording/event/authentication.py:70
|
||||
msgid "Authorization header is required"
|
||||
msgstr "L'en-tête d'autorisation est requis"
|
||||
|
||||
#: core/recording/event/authentication.py:78
|
||||
msgid "Invalid authorization header."
|
||||
msgstr "En-tête d'autorisation invalide."
|
||||
|
||||
#: core/recording/event/authentication.py:88
|
||||
msgid "Invalid token"
|
||||
msgstr "Jeton invalide"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Votre enregistrement est prêt"
|
||||
@@ -455,3 +441,7 @@ msgstr "Français"
|
||||
#: meet/settings.py:164
|
||||
msgid "Dutch"
|
||||
msgstr "Néerlandais"
|
||||
|
||||
#: meet/settings.py:165
|
||||
msgid "German"
|
||||
msgstr "Allemand"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-23 14:04+0000\n"
|
||||
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,112 +29,108 @@ msgstr "Rechten"
|
||||
msgid "Important dates"
|
||||
msgstr "Belangrijke datums"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "No owner"
|
||||
msgstr "Geen eigenaar"
|
||||
|
||||
#: core/admin.py:146
|
||||
msgid "Multiple owners"
|
||||
msgstr "Meerdere eigenaren"
|
||||
|
||||
#: core/api/serializers.py:63
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
||||
|
||||
#: core/authentication/backends.py:77
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr "Gebruikersinformatie bevatte geen herkenbare gebruikersidentificatie"
|
||||
|
||||
#: core/authentication/backends.py:102
|
||||
msgid "User account is disabled"
|
||||
msgstr "Gebruikersaccount is uitgeschakeld"
|
||||
|
||||
#: core/authentication/backends.py:142
|
||||
msgid "Multiple user accounts share a common email."
|
||||
msgstr "Meerdere gebruikersaccounts delen een gemeenschappelijk e-mailadres."
|
||||
|
||||
#: core/models.py:30
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Lid"
|
||||
|
||||
#: core/models.py:31
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Beheerder"
|
||||
|
||||
#: core/models.py:32
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#: core/models.py:48
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Gestart"
|
||||
|
||||
#: core/models.py:49
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Actief"
|
||||
|
||||
#: core/models.py:50
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Gestopt"
|
||||
|
||||
#: core/models.py:51
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Opgeslagen"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Afgebroken"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Starten mislukt"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stoppen mislukt"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notificatie geslaagd"
|
||||
|
||||
#: core/models.py:82
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCHERM_OPNAME"
|
||||
|
||||
#: core/models.py:83
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Openbare toegang"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrouwde toegang"
|
||||
|
||||
#: core/models.py:91
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Beperkte toegang"
|
||||
|
||||
#: core/models.py:103
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:104
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primaire sleutel voor het record als UUID"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "aangemaakt op"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd waarop een record werd aangemaakt"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "bijgewerkt op"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
||||
|
||||
#: core/models.py:137
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -142,66 +138,66 @@ msgstr ""
|
||||
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
|
||||
"_ tekens bevatten."
|
||||
|
||||
#: core/models.py:143
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:145
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Vereist. 255 tekens of minder. Alleen letters, cijfers en @/./+/-/_ tekens."
|
||||
|
||||
#: core/models.py:153
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit e-mailadres"
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "beheerder e-mailadres"
|
||||
|
||||
#: core/models.py:160
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: core/models.py:162
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "korte naam"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: core/models.py:169
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
||||
|
||||
#: core/models.py:178
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "personeelsstatus"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -209,55 +205,63 @@ msgstr ""
|
||||
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
||||
"plaats van accounts te verwijderen."
|
||||
|
||||
#: core/models.py:204
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: core/models.py:205
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: core/models.py:264
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Bron"
|
||||
|
||||
#: core/models.py:265
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Bronnen"
|
||||
|
||||
#: core/models.py:319
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Brontoegang"
|
||||
|
||||
#: core/models.py:320
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Brontoegangsrechten"
|
||||
|
||||
#: core/models.py:326
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
||||
|
||||
#: core/models.py:382
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-ruimteconfiguratie"
|
||||
|
||||
#: core/models.py:383
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
||||
|
||||
#: core/models.py:389 core/models.py:509
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "Pincode van de kamer"
|
||||
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
||||
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Ruimte"
|
||||
|
||||
#: core/models.py:390
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Ruimtes"
|
||||
|
||||
#: core/models.py:520
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:522
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -265,58 +269,42 @@ msgstr ""
|
||||
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
||||
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
||||
|
||||
#: core/models.py:530
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Opnamemodus"
|
||||
|
||||
#: core/models.py:531
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
||||
|
||||
#: core/models.py:537
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Opname"
|
||||
|
||||
#: core/models.py:538
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Opnames"
|
||||
|
||||
#: core/models.py:646
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Opname/gebruiker-relatie"
|
||||
|
||||
#: core/models.py:647
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Opname/gebruiker-relaties"
|
||||
|
||||
#: core/models.py:653
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Deze gebruiker is al in deze opname."
|
||||
|
||||
#: core/models.py:659
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dit team is al in deze opname."
|
||||
|
||||
#: core/models.py:665
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
||||
|
||||
#: core/recording/event/authentication.py:58
|
||||
msgid "Authentication is enabled but token is not configured."
|
||||
msgstr "Authenticatie is ingeschakeld maar token is niet geconfigureerd."
|
||||
|
||||
#: core/recording/event/authentication.py:70
|
||||
msgid "Authorization header is required"
|
||||
msgstr "Autorisatie-header is vereist"
|
||||
|
||||
#: core/recording/event/authentication.py:78
|
||||
msgid "Invalid authorization header."
|
||||
msgstr "Ongeldige autorisatie-header."
|
||||
|
||||
#: core/recording/event/authentication.py:88
|
||||
msgid "Invalid token"
|
||||
msgstr "Ongeldig token"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Je opname is klaar"
|
||||
@@ -447,3 +435,7 @@ msgstr "Frans"
|
||||
#: meet/settings.py:164
|
||||
msgid "Dutch"
|
||||
msgstr "Nederlands"
|
||||
|
||||
#: meet/settings.py:165
|
||||
msgid "German"
|
||||
msgstr "Duits"
|
||||
|
||||
@@ -162,6 +162,7 @@ class Base(Configuration):
|
||||
("en-us", _("English")),
|
||||
("fr-fr", _("French")),
|
||||
("nl-nl", _("Dutch")),
|
||||
("de-de", _("German")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -313,7 +314,7 @@ class Base(Configuration):
|
||||
False, environ_name="FRONTEND_SILENCE_LIVEKIT_DEBUG", environ_prefix=None
|
||||
),
|
||||
"is_silent_login_enabled": values.BooleanValue(
|
||||
True, environ_name="FRONTEND_IS_SILENT_LOGING_ENABLED", environ_prefix=None
|
||||
True, environ_name="FRONTEND_IS_SILENT_LOGIN_ENABLED", environ_prefix=None
|
||||
),
|
||||
"feedback": values.DictValue(
|
||||
{}, environ_name="FRONTEND_FEEDBACK", environ_prefix=None
|
||||
@@ -359,7 +360,9 @@ class Base(Configuration):
|
||||
# Session
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 12
|
||||
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
|
||||
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
|
||||
)
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
|
||||
@@ -572,6 +575,23 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# SIP Telephony
|
||||
ROOM_TELEPHONY_ENABLED = values.BooleanValue(
|
||||
False,
|
||||
environ_name="ROOM_TELEPHONY_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ROOM_TELEPHONY_PIN_LENGTH = values.PositiveIntegerValue(
|
||||
10,
|
||||
environ_name="ROOM_TELEPHONY_PIN_LENGTH",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ROOM_TELEPHONY_PIN_MAX_RETRIES = values.PositiveIntegerValue(
|
||||
5,
|
||||
environ_name="ROOM_TELEPHONY_PIN_MAX_RETRIES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
@@ -618,10 +638,7 @@ class Base(Configuration):
|
||||
release=get_release(),
|
||||
integrations=[DjangoIntegration()],
|
||||
)
|
||||
|
||||
# Add the application name to the Sentry scope
|
||||
scope = sentry_sdk.get_global_scope()
|
||||
scope.set_tag("application", "backend")
|
||||
sentry_sdk.set_tag("application", "backend")
|
||||
|
||||
# Ignore the logs added by the DockerflowMiddleware
|
||||
ignore_logger("request.summary")
|
||||
@@ -785,6 +802,14 @@ class Staging(Production):
|
||||
"""
|
||||
|
||||
|
||||
class Sandbox(Production):
|
||||
"""
|
||||
Sandbox environment settings
|
||||
|
||||
nota bene: it should inherit from the Production environment.
|
||||
"""
|
||||
|
||||
|
||||
class PreProduction(Production):
|
||||
"""
|
||||
Pre-production environment settings
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.21"
|
||||
version = "0.1.23"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -38,7 +38,7 @@ dependencies = [
|
||||
"django-redis==5.4.0",
|
||||
"django-storages[s3]==1.14.5",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.1.8",
|
||||
"django==5.1.9",
|
||||
"djangorestframework==3.15.2",
|
||||
"drf_spectacular==0.28.0",
|
||||
"dockerflow==2024.4.2",
|
||||
|
||||
@@ -35,7 +35,7 @@ ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||
RUN npm run build
|
||||
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_tokens off;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -14,47 +14,47 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.8.1",
|
||||
"@livekit/components-styles": "1.1.4",
|
||||
"@livekit/track-processors": "0.3.3",
|
||||
"@pandacss/preset-panda": "0.53.3",
|
||||
"@react-aria/toast": "3.0.1",
|
||||
"@livekit/components-styles": "1.1.5",
|
||||
"@livekit/track-processors": "0.5.6",
|
||||
"@pandacss/preset-panda": "0.53.6",
|
||||
"@react-aria/toast": "3.0.2",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.69.0",
|
||||
"@tanstack/react-query": "5.76.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"i18next": "24.2.3",
|
||||
"i18next-browser-languagedetector": "8.0.4",
|
||||
"i18next": "25.1.2",
|
||||
"i18next-browser-languagedetector": "8.1.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.9.8",
|
||||
"posthog-js": "1.232.6",
|
||||
"livekit-client": "2.11.4",
|
||||
"posthog-js": "1.240.6",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.7.1",
|
||||
"react-aria-components": "1.8.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.1.4",
|
||||
"wouter": "3.6.0"
|
||||
"valtio": "2.1.5",
|
||||
"wouter": "3.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "0.53.3",
|
||||
"@tanstack/eslint-plugin-query": "5.68.0",
|
||||
"@tanstack/react-query-devtools": "5.69.0",
|
||||
"@types/node": "22.13.13",
|
||||
"@pandacss/dev": "0.53.6",
|
||||
"@tanstack/eslint-plugin-query": "5.74.7",
|
||||
"@tanstack/react-query-devtools": "5.76.0",
|
||||
"@types/node": "22.15.17",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.28.0",
|
||||
"@typescript-eslint/parser": "8.28.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.0",
|
||||
"@typescript-eslint/parser": "8.32.0",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.19",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.3",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.8.2",
|
||||
"vite": "6.2.6",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "6.2.7",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -2,9 +2,11 @@ import { silenceLiveKitLogs } from '@/utils/livekit'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
||||
import { useSupport } from '@/features/support/hooks/useSupport'
|
||||
import { useSyncUserPreferencesWithBackend } from '@/features/auth'
|
||||
|
||||
export const AppInitialization = () => {
|
||||
const { data } = useConfig()
|
||||
useSyncUserPreferencesWithBackend()
|
||||
|
||||
const {
|
||||
analytics = {},
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useNotificationSound } from '@/features/notifications/hooks/useSoundNot
|
||||
import { ToastProvider, toastQueue } from './components/ToastProvider'
|
||||
import { WaitingParticipantNotification } from './components/WaitingParticipantNotification'
|
||||
import {
|
||||
EMOJIS,
|
||||
Emoji,
|
||||
Reaction,
|
||||
} from '@/features/rooms/livekit/components/controls/ReactionsToggle'
|
||||
import {
|
||||
@@ -50,7 +50,7 @@ export const MainNotificationToast = () => {
|
||||
}, [room, triggerNotificationSound])
|
||||
|
||||
const handleEmoji = (emoji: string, participant: Participant) => {
|
||||
if (!emoji || !EMOJIS.includes(emoji)) return
|
||||
if (!emoji || !Object.values(Emoji).includes(emoji as Emoji)) return
|
||||
const id = instanceIdRef.current++
|
||||
setReactions((prev) => [
|
||||
...prev,
|
||||
|
||||
@@ -11,5 +11,6 @@ export const NotificationDuration = {
|
||||
PARTICIPANT_JOINED: ToastDuration.LONG,
|
||||
HAND_RAISED: ToastDuration.LONG,
|
||||
LOWER_HAND: ToastDuration.EXTRA_LONG,
|
||||
RECORDING_SAVING: ToastDuration.EXTRA_LONG,
|
||||
REACTION_RECEIVED: ToastDuration.SHORT,
|
||||
} as const
|
||||
|
||||
@@ -10,4 +10,5 @@ export enum NotificationType {
|
||||
TranscriptionStopped = 'transcriptionStopped',
|
||||
ScreenRecordingStarted = 'screenRecordingStarted',
|
||||
ScreenRecordingStopped = 'screenRecordingStopped',
|
||||
RecordingSaving = 'recordingSaving',
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface ToastData {
|
||||
participant: Participant
|
||||
type: NotificationType
|
||||
message?: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// Using a global queue for toasts allows for centralized management and queuing of notifications
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useToast } from '@react-aria/toast'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { Text } from '@/primitives'
|
||||
|
||||
import { StyledToastContainer, ToastProps } from './Toast'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { RecordingMode } from '@/features/recording'
|
||||
|
||||
export function ToastRecordingSaving({ state, ...props }: ToastProps) {
|
||||
const { t } = useTranslation('notifications', { keyPrefix: 'recordingSave' })
|
||||
const ref = useRef(null)
|
||||
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||
|
||||
const { user } = useUser()
|
||||
|
||||
const modeLabel = useMemo(() => {
|
||||
const mode = props.toast.content.mode as RecordingMode
|
||||
switch (mode) {
|
||||
case RecordingMode.Transcript:
|
||||
return 'transcript'
|
||||
case RecordingMode.ScreenRecording:
|
||||
return 'screenRecording'
|
||||
}
|
||||
}, [props.toast.content])
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<HStack
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
{...contentProps}
|
||||
padding={14}
|
||||
gap={1}
|
||||
>
|
||||
<Text
|
||||
margin={false}
|
||||
className={css({
|
||||
maxWidth: '22rem',
|
||||
wordBreak: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
})}
|
||||
>
|
||||
{user?.email ? (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t(`${modeLabel}.message`, {
|
||||
email: user.email,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
t(`${modeLabel}.default`)
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ToastMuted } from './ToastMuted'
|
||||
import { ToastMessageReceived } from './ToastMessageReceived'
|
||||
import { ToastLowerHand } from './ToastLowerHand'
|
||||
import { ToastAnyRecording } from './ToastAnyRecording'
|
||||
import { ToastRecordingSaving } from './ToastRecordingSaving'
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<ToastData>
|
||||
@@ -43,6 +44,11 @@ const renderToast = (
|
||||
case NotificationType.ScreenRecordingStopped:
|
||||
return <ToastAnyRecording key={toast.key} toast={toast} state={state} />
|
||||
|
||||
case NotificationType.RecordingSaving:
|
||||
return (
|
||||
<ToastRecordingSaving key={toast.key} toast={toast} state={state} />
|
||||
)
|
||||
|
||||
default:
|
||||
return <Toast key={toast.key} toast={toast} state={state} />
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { useNotifyParticipants } from './hooks/useNotifyParticipants'
|
||||
export { NotificationType } from './NotificationType'
|
||||
export { notifyRecordingSaveInProgress } from './utils'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NotificationType } from './NotificationType'
|
||||
import { NotificationDuration } from './NotificationDuration'
|
||||
import { Participant } from 'livekit-client'
|
||||
import { NotificationPayload } from './NotificationPayload'
|
||||
import { RecordingMode } from '@/features/recording'
|
||||
|
||||
export const showLowerHandToast = (
|
||||
participant: Participant,
|
||||
@@ -49,3 +50,17 @@ export const decodeNotificationDataReceived = (
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const notifyRecordingSaveInProgress = (
|
||||
mode: RecordingMode,
|
||||
participant: Participant
|
||||
) => {
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
mode,
|
||||
type: NotificationType.RecordingSaving,
|
||||
},
|
||||
{ timeout: NotificationDuration.RECORDING_SAVING }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,14 +106,10 @@ export const RecordingStateToast = () => {
|
||||
switch (recordingSnap.status) {
|
||||
case RecordingStatus.TRANSCRIPT_STARTED:
|
||||
return 'transcript.started'
|
||||
case RecordingStatus.TRANSCRIPT_STOPPING:
|
||||
return 'transcript.stopping'
|
||||
case RecordingStatus.TRANSCRIPT_STARTING:
|
||||
return 'transcript.starting'
|
||||
case RecordingStatus.SCREEN_RECORDING_STARTED:
|
||||
return 'screenRecording.started'
|
||||
case RecordingStatus.SCREEN_RECORDING_STOPPING:
|
||||
return 'screenRecording.stopping'
|
||||
case RecordingStatus.SCREEN_RECORDING_STARTING:
|
||||
return 'screenRecording.starting'
|
||||
case RecordingStatus.ANY_STARTED:
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CRISP_HELP_ARTICLE_RECORDING } from '@/utils/constants'
|
||||
|
||||
import {
|
||||
NotificationType,
|
||||
notifyRecordingSaveInProgress,
|
||||
useNotifyParticipants,
|
||||
} from '@/features/notifications'
|
||||
import posthog from 'posthog-js'
|
||||
@@ -84,6 +85,10 @@ export const ScreenRecordingSidePanel = () => {
|
||||
await notifyParticipants({
|
||||
type: NotificationType.ScreenRecordingStopped,
|
||||
})
|
||||
notifyRecordingSaveInProgress(
|
||||
RecordingMode.ScreenRecording,
|
||||
room.localParticipant
|
||||
)
|
||||
} else {
|
||||
await startRecordingRoom({
|
||||
id: roomId,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useIsRecordingTransitioning,
|
||||
useStartRecording,
|
||||
useStopRecording,
|
||||
useHasFeatureWithoutAdminRights,
|
||||
} from '../index'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { ConnectionState, RoomEvent } from 'livekit-client'
|
||||
@@ -23,6 +24,7 @@ import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import {
|
||||
NotificationType,
|
||||
useNotifyParticipants,
|
||||
notifyRecordingSaveInProgress,
|
||||
} from '@/features/notifications'
|
||||
import posthog from 'posthog-js'
|
||||
import { useSnapshot } from 'valtio/index'
|
||||
@@ -42,6 +44,12 @@ export const TranscriptSidePanel = () => {
|
||||
RecordingMode.Transcript,
|
||||
FeatureFlags.Transcript
|
||||
)
|
||||
|
||||
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
|
||||
RecordingMode.Transcript,
|
||||
FeatureFlags.Transcript
|
||||
)
|
||||
|
||||
const roomId = useRoomId()
|
||||
|
||||
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
|
||||
@@ -92,6 +100,10 @@ export const TranscriptSidePanel = () => {
|
||||
await notifyParticipants({
|
||||
type: NotificationType.TranscriptionStopped,
|
||||
})
|
||||
notifyRecordingSaveInProgress(
|
||||
RecordingMode.Transcript,
|
||||
room.localParticipant
|
||||
)
|
||||
} else {
|
||||
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
|
||||
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
|
||||
@@ -134,30 +146,54 @@ export const TranscriptSidePanel = () => {
|
||||
/>
|
||||
{!hasTranscriptAccess ? (
|
||||
<>
|
||||
<Text>{t('beta.heading')}</Text>
|
||||
<Text
|
||||
variant="note"
|
||||
wrap={'pretty'}
|
||||
centered
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
marginBottom: '2.5rem',
|
||||
marginTop: '0.25rem',
|
||||
})}
|
||||
>
|
||||
{t('beta.body')}{' '}
|
||||
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
|
||||
{t('start.linkMore')}
|
||||
</A>
|
||||
</Text>
|
||||
<LinkButton
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
href={BETA_USERS_FORM_URL}
|
||||
target="_blank"
|
||||
>
|
||||
{t('beta.button')}
|
||||
</LinkButton>
|
||||
{hasFeatureWithoutAdminRights ? (
|
||||
<>
|
||||
<Text>{t('notAdminOrOwner.heading')}</Text>
|
||||
<Text
|
||||
variant="note"
|
||||
wrap="balance"
|
||||
centered
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
marginBottom: '2.5rem',
|
||||
marginTop: '0.25rem',
|
||||
})}
|
||||
>
|
||||
{t('notAdminOrOwner.body')}
|
||||
<br />
|
||||
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
|
||||
{t('notAdminOrOwner.linkMore')}
|
||||
</A>
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>{t('beta.heading')}</Text>
|
||||
<Text
|
||||
variant="note"
|
||||
wrap={'pretty'}
|
||||
centered
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
marginBottom: '2.5rem',
|
||||
marginTop: '0.25rem',
|
||||
})}
|
||||
>
|
||||
{t('beta.body')}{' '}
|
||||
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
|
||||
{t('start.linkMore')}
|
||||
</A>
|
||||
</Text>
|
||||
<LinkButton
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
href={BETA_USERS_FORM_URL}
|
||||
target="_blank"
|
||||
>
|
||||
{t('beta.button')}
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
import { RecordingMode } from '../types'
|
||||
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
|
||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
|
||||
export const useHasFeatureWithoutAdminRights = (
|
||||
mode: RecordingMode,
|
||||
featureFlag: FeatureFlags
|
||||
) => {
|
||||
const featureEnabled = useFeatureFlagEnabled(featureFlag)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
|
||||
const isAdminOrOwner = useIsAdminOrOwner()
|
||||
|
||||
return (
|
||||
(featureEnabled || !isAnalyticsEnabled) &&
|
||||
isRecordingModeEnabled &&
|
||||
!isAdminOrOwner
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
|
||||
export { useIsRecordingTransitioning } from './hooks/useIsRecordingTransitioning'
|
||||
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
|
||||
export { useIsRecordingActive } from './hooks/useIsRecordingActive'
|
||||
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
|
||||
|
||||
// api
|
||||
export { useStartRecording } from './api/startRecording'
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export { Room as RoomRoute } from './routes/Room'
|
||||
export { FeedbackRoute } from './routes/Feedback'
|
||||
export { roomIdPattern, isRoomValid } from './utils/isRoomValid'
|
||||
export {
|
||||
roomIdPattern,
|
||||
isRoomValid,
|
||||
flexibleRoomIdPattern,
|
||||
} from './utils/isRoomValid'
|
||||
export { generateRoomId } from './utils/generateRoomId'
|
||||
export { useCreateRoom } from './api/createRoom'
|
||||
|
||||
@@ -15,6 +15,7 @@ export const INITIAL_POSITION = 200
|
||||
interface FloatingReactionProps {
|
||||
emoji: string
|
||||
name?: string
|
||||
isLocal?: boolean
|
||||
speed?: number
|
||||
scale?: number
|
||||
}
|
||||
@@ -22,6 +23,7 @@ interface FloatingReactionProps {
|
||||
export function FloatingReaction({
|
||||
emoji,
|
||||
name,
|
||||
isLocal = false,
|
||||
speed = 1,
|
||||
scale = 1,
|
||||
}: FloatingReactionProps) {
|
||||
@@ -60,7 +62,6 @@ export function FloatingReaction({
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3rem',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -69,23 +70,27 @@ export function FloatingReaction({
|
||||
style={{
|
||||
left: left,
|
||||
bottom: INITIAL_POSITION + deltaY,
|
||||
transform: `scale(${scale})`,
|
||||
opacity: opacity,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
<img
|
||||
src={`/assets/reactions/${emoji}.png`}
|
||||
alt={''}
|
||||
className={css({
|
||||
lineHeight: '57px',
|
||||
height: '50px',
|
||||
})}
|
||||
>
|
||||
{emoji}
|
||||
</span>
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center bottom',
|
||||
}}
|
||||
/>
|
||||
{name && (
|
||||
<Text
|
||||
variant="sm"
|
||||
className={css({
|
||||
backgroundColor: 'primaryDark.100',
|
||||
color: 'white',
|
||||
backgroundColor: isLocal ? 'primary.100' : 'primaryDark.100',
|
||||
color: isLocal ? 'black' : 'white',
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
borderRadius: '20px',
|
||||
paddingX: '0.5rem',
|
||||
@@ -128,6 +133,7 @@ export function ReactionPortal({
|
||||
speed={speed}
|
||||
scale={scale}
|
||||
name={participant?.isLocal ? t('you') : participant.name}
|
||||
isLocal={participant?.isLocal}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ProcessorOptions, Track } from 'livekit-client'
|
||||
import { ProcessorOptions, Track, TrackProcessor } from 'livekit-client'
|
||||
import posthog from 'posthog-js'
|
||||
import {
|
||||
FilesetResolver,
|
||||
@@ -11,19 +11,20 @@ import {
|
||||
TIMEOUT_TICK,
|
||||
timerWorkerScript,
|
||||
} from './TimerWorker'
|
||||
import {
|
||||
BackgroundProcessorInterface,
|
||||
BackgroundOptions,
|
||||
ProcessorType,
|
||||
} from '.'
|
||||
import { ProcessorType } from '.'
|
||||
|
||||
const PROCESSING_WIDTH = 256 * 3
|
||||
const PROCESSING_HEIGHT = 144 * 3
|
||||
|
||||
const FACE_LANDMARKS_CANVAS_ID = 'face-landmarks-local'
|
||||
|
||||
export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
|
||||
options: BackgroundOptions
|
||||
export type FaceLandmarksOptions = {
|
||||
showGlasses: boolean
|
||||
showFrench: boolean
|
||||
}
|
||||
|
||||
export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
|
||||
options: FaceLandmarksOptions
|
||||
name: string
|
||||
processedTrack?: MediaStreamTrack | undefined
|
||||
|
||||
@@ -50,7 +51,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
|
||||
glassesImage?: HTMLImageElement
|
||||
mustacheImage?: HTMLImageElement
|
||||
beretImage?: HTMLImageElement
|
||||
constructor(opts: BackgroundOptions) {
|
||||
constructor(opts: FaceLandmarksOptions) {
|
||||
this.name = 'face_landmarks'
|
||||
this.options = opts
|
||||
this.type = ProcessorType.FACE_LANDMARKS
|
||||
@@ -314,7 +315,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
|
||||
return element
|
||||
}
|
||||
|
||||
update(opts: BackgroundOptions): void {
|
||||
update(opts: FaceLandmarksOptions): void {
|
||||
this.options = opts
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,10 @@ import { Track, TrackProcessor } from 'livekit-client'
|
||||
import { BackgroundBlurTrackProcessorJsWrapper } from './BackgroundBlurTrackProcessorJsWrapper'
|
||||
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
|
||||
import { BackgroundVirtualTrackProcessorJsWrapper } from './BackgroundVirtualTrackProcessorJsWrapper'
|
||||
import { FaceLandmarksProcessor } from './FaceLandmarksProcessor'
|
||||
|
||||
export type BackgroundOptions = {
|
||||
blurRadius?: number
|
||||
imagePath?: string
|
||||
showGlasses?: boolean
|
||||
showFrench?: boolean
|
||||
}
|
||||
|
||||
export interface ProcessorSerialized {
|
||||
@@ -32,12 +29,12 @@ export enum ProcessorType {
|
||||
}
|
||||
|
||||
export class BackgroundProcessorFactory {
|
||||
static hasModernApiSupport() {
|
||||
return ProcessorWrapper.hasModernApiSupport
|
||||
}
|
||||
|
||||
static isSupported() {
|
||||
return (
|
||||
ProcessorWrapper.isSupported ||
|
||||
BackgroundCustomProcessor.isSupported ||
|
||||
FaceLandmarksProcessor.isSupported
|
||||
)
|
||||
return ProcessorWrapper.isSupported || BackgroundCustomProcessor.isSupported
|
||||
}
|
||||
|
||||
static getProcessor(
|
||||
@@ -58,10 +55,6 @@ export class BackgroundProcessorFactory {
|
||||
if (BackgroundCustomProcessor.isSupported) {
|
||||
return new BackgroundCustomProcessor(opts)
|
||||
}
|
||||
} else if (type === ProcessorType.FACE_LANDMARKS) {
|
||||
if (FaceLandmarksProcessor.isSupported) {
|
||||
return new FaceLandmarksProcessor(opts)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -15,7 +15,16 @@ import { Participant } from 'livekit-client'
|
||||
import useRateLimiter from '@/hooks/useRateLimiter'
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const EMOJIS = ['👍', '👎', '👏', '❤️', '😂', '😮', '🎉']
|
||||
export enum Emoji {
|
||||
THUMBS_UP = 'thumbs-up',
|
||||
THUMBS_DOWN = 'thumbs-down',
|
||||
CLAP = 'clapping-hands',
|
||||
HEART = 'red-heart',
|
||||
LAUGHING = 'face-with-tears-of-joy',
|
||||
SURPRISED = 'face-with-open-mouth',
|
||||
CELEBRATION = 'party-popper',
|
||||
PLEASE = 'folded-hands',
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
id: number
|
||||
@@ -112,10 +121,10 @@ export const ReactionsToggle = () => {
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: -55,
|
||||
left: -114,
|
||||
top: -63,
|
||||
left: -162,
|
||||
borderRadius: '8px',
|
||||
padding: '0.25rem',
|
||||
padding: '0.35rem',
|
||||
backgroundColor: 'primaryDark.50',
|
||||
opacity: opacity,
|
||||
transition: 'opacity 0.2s ease',
|
||||
@@ -129,24 +138,29 @@ export const ReactionsToggle = () => {
|
||||
<RACToolbar
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
{EMOJIS.map((emoji, index) => (
|
||||
{Object.values(Emoji).map((emoji, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onPress={() => debouncedSendReaction(emoji)}
|
||||
aria-label={t('send', { emoji })}
|
||||
variant="primaryTextDark"
|
||||
size="sm"
|
||||
square
|
||||
data-attr={`send-reaction-${emoji}`}
|
||||
>
|
||||
<span
|
||||
<img
|
||||
src={`/assets/reactions/${emoji}.png`}
|
||||
alt=""
|
||||
className={css({
|
||||
fontSize: '20px',
|
||||
minHeight: '28px',
|
||||
minWidth: '28px',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
})}
|
||||
>
|
||||
{emoji}
|
||||
</span>
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</RACToolbar>
|
||||
|
||||
@@ -15,12 +15,9 @@ import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
|
||||
import { useTrackToggle } from '@livekit/components-react'
|
||||
import { Loader } from '@/primitives/Loader'
|
||||
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
|
||||
import {
|
||||
RiProhibited2Line,
|
||||
RiGlassesLine,
|
||||
RiGoblet2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess'
|
||||
import { RiProhibited2Line } from '@remixicon/react'
|
||||
import { FunnyEffects } from './FunnyEffects'
|
||||
import { useHasFunnyEffectsAccess } from '../../hooks/useHasFunnyEffectsAccess'
|
||||
|
||||
enum BlurRadius {
|
||||
NONE = 0,
|
||||
@@ -55,7 +52,7 @@ export const EffectsConfiguration = ({
|
||||
const { toggle, enabled } = useTrackToggle({ source: Track.Source.Camera })
|
||||
const [processorPending, setProcessorPending] = useState(false)
|
||||
const processorPendingReveal = useSyncAfterDelay(processorPending)
|
||||
const hasFaceLandmarksAccess = useHasFaceLandmarksAccess()
|
||||
const hasFunnyEffectsAccess = useHasFunnyEffectsAccess()
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current
|
||||
@@ -114,6 +111,13 @@ export const EffectsConfiguration = ({
|
||||
type,
|
||||
options
|
||||
)!
|
||||
// IMPORTANT: Must explicitly stop previous processor before setting a new one
|
||||
// in browsers without modern API support to prevent UI crashes.
|
||||
// This workaround is needed until this issue is resolved:
|
||||
// https://github.com/livekit/track-processors-js/issues/85
|
||||
if (!BackgroundProcessorFactory.hasModernApiSupport()) {
|
||||
await videoTrack.stopProcessor()
|
||||
}
|
||||
await videoTrack.setProcessor(newProcessor)
|
||||
onSubmit?.(newProcessor)
|
||||
} else {
|
||||
@@ -145,42 +149,9 @@ export const EffectsConfiguration = ({
|
||||
}
|
||||
|
||||
const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => {
|
||||
if (type === ProcessorType.FACE_LANDMARKS) {
|
||||
const effect = options.showGlasses ? 'glasses' : 'french'
|
||||
return t(
|
||||
`faceLandmarks.${effect}.${isSelected(type, options) ? 'clear' : 'apply'}`
|
||||
)
|
||||
}
|
||||
return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`)
|
||||
}
|
||||
|
||||
const getFaceLandmarksOptions = () => {
|
||||
const processor = getProcessor()
|
||||
if (processor?.serialize().type === ProcessorType.FACE_LANDMARKS) {
|
||||
return processor.serialize().options as {
|
||||
showGlasses?: boolean
|
||||
showFrench?: boolean
|
||||
}
|
||||
}
|
||||
return { showGlasses: false, showFrench: false }
|
||||
}
|
||||
|
||||
const toggleFaceLandmarkEffect = async (effect: 'glasses' | 'french') => {
|
||||
const currentOptions = getFaceLandmarksOptions()
|
||||
const newOptions = {
|
||||
...currentOptions,
|
||||
[effect === 'glasses' ? 'showGlasses' : 'showFrench']:
|
||||
!currentOptions[effect === 'glasses' ? 'showGlasses' : 'showFrench'],
|
||||
}
|
||||
|
||||
if (!newOptions.showGlasses && !newOptions.showFrench) {
|
||||
// If both effects are off stop the processor
|
||||
await clearEffect()
|
||||
} else {
|
||||
await toggleEffect(ProcessorType.FACE_LANDMARKS, newOptions)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css(
|
||||
@@ -215,7 +186,7 @@ export const EffectsConfiguration = ({
|
||||
muted
|
||||
style={{
|
||||
transform: 'rotateY(180deg)',
|
||||
minHeight: '175px',
|
||||
[layout === 'vertical' ? 'height' : 'minHeight']: '175px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
/>
|
||||
@@ -268,6 +239,13 @@ export const EffectsConfiguration = ({
|
||||
: {}
|
||||
)}
|
||||
>
|
||||
{hasFunnyEffectsAccess && (
|
||||
<FunnyEffects
|
||||
videoTrack={videoTrack}
|
||||
isPending={processorPendingReveal}
|
||||
onPending={setProcessorPending}
|
||||
/>
|
||||
)}
|
||||
{isSupported ? (
|
||||
<>
|
||||
<div>
|
||||
@@ -340,8 +318,6 @@ export const EffectsConfiguration = ({
|
||||
<BlurOnStrong />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</div>
|
||||
{hasFaceLandmarksAccess && (
|
||||
<div
|
||||
className={css({
|
||||
marginTop: '1.5rem',
|
||||
@@ -354,114 +330,51 @@ export const EffectsConfiguration = ({
|
||||
}}
|
||||
variant="bodyXsBold"
|
||||
>
|
||||
{t('faceLandmarks.title')}
|
||||
{t('virtual.title')}
|
||||
</H>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1.25rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
|
||||
showGlasses: true,
|
||||
showFrench: false,
|
||||
})}
|
||||
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
|
||||
showGlasses: true,
|
||||
showFrench: false,
|
||||
})}
|
||||
isDisabled={processorPendingReveal}
|
||||
onChange={async () =>
|
||||
await toggleFaceLandmarkEffect('glasses')
|
||||
}
|
||||
isSelected={getFaceLandmarksOptions().showGlasses}
|
||||
data-attr="toggle-glasses"
|
||||
>
|
||||
<RiGlassesLine />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
|
||||
showGlasses: false,
|
||||
showFrench: true,
|
||||
})}
|
||||
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
|
||||
showGlasses: false,
|
||||
showFrench: true,
|
||||
})}
|
||||
isDisabled={processorPendingReveal}
|
||||
onChange={async () =>
|
||||
await toggleFaceLandmarkEffect('french')
|
||||
}
|
||||
isSelected={getFaceLandmarksOptions().showFrench}
|
||||
data-attr="toggle-french"
|
||||
>
|
||||
<RiGoblet2Fill />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={css({
|
||||
marginTop: '1.5rem',
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={3}
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
variant="bodyXsBold"
|
||||
>
|
||||
{t('virtual.title')}
|
||||
</H>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1.25rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
{[...Array(8).keys()].map((i) => {
|
||||
const imagePath = `/assets/backgrounds/${i + 1}.jpg`
|
||||
const thumbnailPath = `/assets/backgrounds/thumbnails/${i + 1}.jpg`
|
||||
return (
|
||||
<ToggleButton
|
||||
key={i}
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipLabel(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})}
|
||||
tooltip={tooltipLabel(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})}
|
||||
isDisabled={processorPendingReveal}
|
||||
onChange={async () =>
|
||||
await toggleEffect(ProcessorType.VIRTUAL, {
|
||||
{[...Array(8).keys()].map((i) => {
|
||||
const imagePath = `/assets/backgrounds/${i + 1}.jpg`
|
||||
const thumbnailPath = `/assets/backgrounds/thumbnails/${i + 1}.jpg`
|
||||
return (
|
||||
<ToggleButton
|
||||
key={i}
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipLabel(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})
|
||||
}
|
||||
isSelected={isSelected(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${i}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
})}
|
||||
tooltip={tooltipLabel(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})}
|
||||
isDisabled={processorPendingReveal}
|
||||
onChange={async () =>
|
||||
await toggleEffect(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})
|
||||
}
|
||||
isSelected={isSelected(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${i}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Information className={css({ marginTop: '1rem' })}>
|
||||
<Text variant="sm">⚠︎ {t('experimental')}</Text>
|
||||
</Information>
|
||||
</>
|
||||
) : (
|
||||
<Information>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { H, ToggleButton } from '@/primitives'
|
||||
import { ProcessorType } from '../blur'
|
||||
import { RiGlassesLine, RiGoblet2Fill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor'
|
||||
import { LocalVideoTrack } from 'livekit-client'
|
||||
|
||||
export type FunnyEffectsProps = {
|
||||
videoTrack: LocalVideoTrack
|
||||
isPending?: boolean
|
||||
onPending: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const FunnyEffects = ({
|
||||
videoTrack,
|
||||
isPending,
|
||||
onPending,
|
||||
}: FunnyEffectsProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
|
||||
|
||||
const getOptions = () => {
|
||||
const processor = videoTrack?.getProcessor() as FaceLandmarksProcessor
|
||||
if (!processor || processor.type != ProcessorType.FACE_LANDMARKS) {
|
||||
return {
|
||||
showGlasses: false,
|
||||
showFrench: false,
|
||||
}
|
||||
}
|
||||
return processor.serialize().options
|
||||
}
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
const toggleFaceLandmarkEffect = async (
|
||||
showEffect: 'showGlasses' | 'showFrench'
|
||||
) => {
|
||||
const options = getOptions()
|
||||
const processor = videoTrack?.getProcessor() as FaceLandmarksProcessor
|
||||
|
||||
const newOptions = {
|
||||
...options,
|
||||
[showEffect]: !options[showEffect],
|
||||
}
|
||||
|
||||
onPending(true)
|
||||
|
||||
try {
|
||||
if (!newOptions.showGlasses && !newOptions.showFrench) {
|
||||
await videoTrack.stopProcessor()
|
||||
} else if (options.showGlasses || options.showFrench) {
|
||||
await processor?.update(newOptions)
|
||||
} else {
|
||||
const newProcessor = new FaceLandmarksProcessor(newOptions)
|
||||
await videoTrack.setProcessor(newProcessor)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('could not update processor', e)
|
||||
} finally {
|
||||
onPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getLabelAction = (enabled: boolean) => (enabled ? 'clear' : 'apply')
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
marginBottom: '1.5rem',
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={3}
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
variant="bodyXsBold"
|
||||
>
|
||||
{t('faceLandmarks.title')}
|
||||
</H>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1.25rem',
|
||||
})}
|
||||
>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={t(
|
||||
`faceLandmarks.glasses.${getLabelAction(options.showGlasses)}`
|
||||
)}
|
||||
tooltip={t(
|
||||
`faceLandmarks.glasses.${getLabelAction(options.showGlasses)}`
|
||||
)}
|
||||
isDisabled={isPending}
|
||||
onChange={async () => await toggleFaceLandmarkEffect('showGlasses')}
|
||||
isSelected={options.showGlasses}
|
||||
data-attr="toggle-glasses"
|
||||
>
|
||||
<RiGlassesLine />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={t(
|
||||
`faceLandmarks.french.${getLabelAction(options.showFrench)}`
|
||||
)}
|
||||
tooltip={t(
|
||||
`faceLandmarks.french.${getLabelAction(options.showFrench)}`
|
||||
)}
|
||||
isDisabled={isPending}
|
||||
onChange={async () => await toggleFaceLandmarkEffect('showFrench')}
|
||||
isSelected={options.showFrench}
|
||||
data-attr="toggle-french"
|
||||
>
|
||||
<RiGoblet2Fill />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
|
||||
export const useHasFaceLandmarksAccess = () => {
|
||||
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
return featureEnabled || !isAnalyticsEnabled
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import useKonami from '@/features/rooms/livekit/hooks/useKonami'
|
||||
import { konamiStore } from '@/stores/konami'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
export const useHasFunnyEffectsAccess = () => {
|
||||
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
const konamiSnap = useSnapshot(konamiStore)
|
||||
|
||||
useKonami(
|
||||
() =>
|
||||
(konamiStore.areFunnyEffectsEnabled = !konamiSnap.areFunnyEffectsEnabled)
|
||||
)
|
||||
|
||||
return (
|
||||
(featureEnabled || !isAnalyticsEnabled) && konamiSnap.areFunnyEffectsEnabled
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Konami Code Detector Component
|
||||
* This implementation is taken from: vmarchesin/react-konami-code
|
||||
*/
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]
|
||||
|
||||
function useKonami(action: () => void, { code = KONAMI_CODE } = {}) {
|
||||
const [input, setInput] = useState<number[]>([])
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const newInput = input
|
||||
newInput.push(e.keyCode)
|
||||
newInput.splice(-code.length - 1, input.length - code.length)
|
||||
|
||||
setInput(newInput)
|
||||
|
||||
if (newInput.join('').includes(code.join(''))) {
|
||||
action()
|
||||
}
|
||||
},
|
||||
[input, setInput, code, action]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', onKeyUp)
|
||||
return () => {
|
||||
document.removeEventListener('keyup', onKeyUp)
|
||||
}
|
||||
}, [onKeyUp])
|
||||
}
|
||||
|
||||
export default useKonami
|
||||
@@ -19,23 +19,38 @@ export function usePersistentUserChoices(
|
||||
)
|
||||
|
||||
const saveAudioInputEnabled = React.useCallback((isEnabled: boolean) => {
|
||||
setSettings((prev) => ({ ...prev, audioEnabled: isEnabled }))
|
||||
setSettings((prev: LocalUserChoices) => ({
|
||||
...prev,
|
||||
audioEnabled: isEnabled,
|
||||
}))
|
||||
}, [])
|
||||
const saveVideoInputEnabled = React.useCallback((isEnabled: boolean) => {
|
||||
setSettings((prev) => ({ ...prev, videoEnabled: isEnabled }))
|
||||
setSettings((prev: LocalUserChoices) => ({
|
||||
...prev,
|
||||
videoEnabled: isEnabled,
|
||||
}))
|
||||
}, [])
|
||||
const saveAudioInputDeviceId = React.useCallback((deviceId: string) => {
|
||||
setSettings((prev) => ({ ...prev, audioDeviceId: deviceId }))
|
||||
setSettings((prev: LocalUserChoices) => ({
|
||||
...prev,
|
||||
audioDeviceId: deviceId,
|
||||
}))
|
||||
}, [])
|
||||
const saveVideoInputDeviceId = React.useCallback((deviceId: string) => {
|
||||
setSettings((prev) => ({ ...prev, videoDeviceId: deviceId }))
|
||||
setSettings((prev: LocalUserChoices) => ({
|
||||
...prev,
|
||||
videoDeviceId: deviceId,
|
||||
}))
|
||||
}, [])
|
||||
const saveUsername = React.useCallback((username: string) => {
|
||||
setSettings((prev) => ({ ...prev, username: username }))
|
||||
setSettings((prev: LocalUserChoices) => ({ ...prev, username: username }))
|
||||
}, [])
|
||||
const saveProcessorSerialized = React.useCallback(
|
||||
(processorSerialized?: ProcessorSerialized) => {
|
||||
setSettings((prev) => ({ ...prev, processorSerialized }))
|
||||
setSettings((prev: LocalUserChoices) => ({
|
||||
...prev,
|
||||
processorSerialized,
|
||||
}))
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -3,13 +3,17 @@ import {
|
||||
usePersistentUserChoices,
|
||||
type LocalUserChoices as LocalUserChoicesLK,
|
||||
} from '@livekit/components-react'
|
||||
import { useParams } from 'wouter'
|
||||
import { useLocation, useParams } from 'wouter'
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { useUser, UserAware } from '@/features/auth'
|
||||
import { Conference } from '../components/Conference'
|
||||
import { Join } from '../components/Join'
|
||||
import { useKeyboardShortcuts } from '@/features/shortcuts/useKeyboardShortcuts'
|
||||
import { ProcessorSerialized } from '../livekit/components/blur'
|
||||
import {
|
||||
isRoomValid,
|
||||
normalizeRoomId,
|
||||
} from '@/features/rooms/utils/isRoomValid'
|
||||
|
||||
export type LocalUserChoices = LocalUserChoicesLK & {
|
||||
processorSerialized?: ProcessorSerialized
|
||||
@@ -21,6 +25,7 @@ export const Room = () => {
|
||||
const [userConfig, setUserConfig] = useState<LocalUserChoices | null>(null)
|
||||
|
||||
const { roomId } = useParams()
|
||||
const [location, setLocation] = useLocation()
|
||||
const initialRoomData = history.state?.initialRoomData
|
||||
const mode = isLoggedIn && history.state?.create ? 'create' : 'join'
|
||||
const skipJoinScreen = isLoggedIn && mode === 'create'
|
||||
@@ -40,6 +45,12 @@ export const Room = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (roomId && !isRoomValid(roomId)) {
|
||||
setLocation(normalizeRoomId(roomId))
|
||||
}
|
||||
}, [roomId, setLocation, location])
|
||||
|
||||
if (!roomId) {
|
||||
return <ErrorScreen />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
export const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
|
||||
|
||||
// Case-insensitive and with optional hyphens
|
||||
export const flexibleRoomIdPattern =
|
||||
'(?:[a-zA-Z0-9]{3}-?[a-zA-Z0-9]{4}-?[a-zA-Z0-9]{3})'
|
||||
|
||||
export const isRoomValid = (roomIdOrUrl: string) =>
|
||||
new RegExp(`^${roomIdPattern}$`).test(roomIdOrUrl) ||
|
||||
new RegExp(`^${window.location.origin}/${roomIdPattern}$`).test(roomIdOrUrl)
|
||||
|
||||
export const normalizeRoomId = (roomId: string) => {
|
||||
const cleanId = roomId.toLowerCase().replace(/-/g, '')
|
||||
if (cleanId.length === 10) {
|
||||
return `${cleanId.slice(0, 3)}-${cleanId.slice(3, 7)}-${cleanId.slice(7, 10)}`
|
||||
}
|
||||
return roomId
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ i18n
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
supportedLngs: ['en', 'fr', 'nl'],
|
||||
supportedLngs: ['en', 'fr', 'nl', 'de'],
|
||||
fallbackLng: 'fr',
|
||||
ns: i18nDefaultNamespace,
|
||||
detection: {
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
{
|
||||
"accessibility": {
|
||||
"title": "",
|
||||
"introduction": "",
|
||||
"title": "Barrierefreiheit",
|
||||
"introduction": "<i>Visio</i> verpflichtet sich, seine digitalen Dienste barrierefrei zu gestalten, gemäß Artikel 47 des Gesetzes Nr. 2005-102 vom 11. Februar 2005.",
|
||||
"declaration": {
|
||||
"title": "",
|
||||
"date": ""
|
||||
"title": "Erklärung zur Barrierefreiheit",
|
||||
"date": "Erstellt am 4. Dezember 2024."
|
||||
},
|
||||
"scope": "",
|
||||
"scope": "Diese Erklärung zur Barrierefreiheit gilt für die Website visio.numerique.gouv.fr",
|
||||
"complianceStatus": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Stand der Vereinbarkeit",
|
||||
"body": "visio.numerique.gouv.fr ist nicht konform mit RGAA 4.1. Die Website wurde noch nicht geprüft. Das Team bemüht sich jedoch, eine für alle zugängliche Seite gemäß den Empfehlungen des RGAA zu erstellen."
|
||||
},
|
||||
"improvement": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"title": "Verbesserung und Kontakt",
|
||||
"body": "Wenn Sie auf Inhalte oder Dienste nicht zugreifen können, können Sie die verantwortliche Person von lasuite.numerique.gouv.fr kontaktieren, um eine barrierefreie Alternative zu erhalten oder die Inhalte in einer anderen Form zu bekommen.",
|
||||
"contact": {
|
||||
"email": "",
|
||||
"address": ""
|
||||
"email": "E-Mail: visio@numerique.gouv.fr",
|
||||
"address": "Adresse: DINUM, 20 avenue de Ségur, 75007 Paris"
|
||||
},
|
||||
"response": ""
|
||||
"response": "Wir bemühen uns, innerhalb von 2 Werktagen zu antworten."
|
||||
},
|
||||
"recourse": {
|
||||
"title": "",
|
||||
"introduction": "",
|
||||
"title": "Rechtsbehelfe",
|
||||
"introduction": "Dieses Verfahren ist in folgendem Fall zu verwenden: Sie haben der für die Website zuständigen Stelle ein Problem mit der Barrierefreiheit gemeldet, das Sie daran hindert, auf Inhalte oder Dienste des Portals zuzugreifen, und keine zufriedenstellende Antwort erhalten.",
|
||||
"options": {
|
||||
"intro": "",
|
||||
"option1": "",
|
||||
"option2": "",
|
||||
"option3": ""
|
||||
"intro": "Sie können:",
|
||||
"option1": "Dem*der Beauftragten für Rechte eine Nachricht schreiben",
|
||||
"option2": "Den*die Vertreter*in des*der Beauftragten für Rechte in Ihrer Region kontaktieren",
|
||||
"option3": "Einen Brief per Post senden (kostenfrei, ohne Briefmarke): </br> Défenseur des droits, Libre réponse 71120, 75342 Paris CEDEX 07"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
{
|
||||
"app": "Visio",
|
||||
"backToHome": "",
|
||||
"cancel": "",
|
||||
"closeDialog": "",
|
||||
"backToHome": "Zurück zur Startseite",
|
||||
"cancel": "Abbrechen",
|
||||
"closeDialog": "Dialogfenster schließen",
|
||||
"error": {
|
||||
"heading": ""
|
||||
"heading": "Beim Laden der Seite ist ein Fehler aufgetreten"
|
||||
},
|
||||
"feedback": {
|
||||
"context": "",
|
||||
"cta": ""
|
||||
"context": "Produkt in Entwicklung — Ihr Feedback ist wichtig!",
|
||||
"cta": "Teilen Sie uns Ihre Meinung mit"
|
||||
},
|
||||
"forbidden": {
|
||||
"heading": ""
|
||||
"heading": "Zugriff verweigert"
|
||||
},
|
||||
"loading": "",
|
||||
"loggedInUserTooltip": "",
|
||||
"loading": "Ladevorgang…",
|
||||
"loggedInUserTooltip": "Angemeldet als…",
|
||||
"login": {
|
||||
"buttonLabel": "",
|
||||
"linkLabel": "",
|
||||
"link": ""
|
||||
"buttonLabel": "Mit ProConnect anmelden",
|
||||
"linkLabel": "Was ist ProConnect? – neues Fenster",
|
||||
"link": "Was ist ProConnect?"
|
||||
},
|
||||
"logout": "",
|
||||
"logout": "Abmelden",
|
||||
"notFound": {
|
||||
"heading": "",
|
||||
"body": ""
|
||||
"heading": "Überprüfen Sie Ihren Meeting-Code",
|
||||
"body": "Stellen Sie sicher, dass Sie den richtigen Meeting-Code in der URL eingegeben haben. Beispiel:"
|
||||
},
|
||||
"submit": "OK",
|
||||
"footer": {
|
||||
"links": {
|
||||
"legifrance": "",
|
||||
"infogouv": "",
|
||||
"servicepublic": "",
|
||||
"datagouv": "",
|
||||
"legalsTerms": "",
|
||||
"data": "",
|
||||
"accessibility": "",
|
||||
"ariaLabel": "",
|
||||
"codeAnnotation": "",
|
||||
"code": "",
|
||||
"technicalDetails": "",
|
||||
"termsOfService": ""
|
||||
"legifrance": "legifrance.gouv.fr",
|
||||
"infogouv": "info.gouv.fr",
|
||||
"servicepublic": "service-public.fr",
|
||||
"datagouv": "data.gouv.fr",
|
||||
"legalsTerms": "Rechtliche Hinweise",
|
||||
"data": "Datenschutz und Cookies",
|
||||
"accessibility": "Barrierefreiheit: nicht konform",
|
||||
"ariaLabel": "neues Fenster",
|
||||
"codeAnnotation": "Unser Code ist offen und verfügbar in diesem",
|
||||
"code": "Open-Source-Code-Repository",
|
||||
"technicalDetails": "Technisches Datenblatt",
|
||||
"termsOfService": "Nutzungsbedingungen"
|
||||
},
|
||||
"mentions": "",
|
||||
"license": ""
|
||||
"mentions": "Sofern nicht anders angegeben, sind die Inhalte dieser Website verfügbar unter der",
|
||||
"license": "Etalab 2.0 Lizenz"
|
||||
},
|
||||
"loginHint": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"title": "Melden Sie sich mit Ihrem ProConnect-Konto an",
|
||||
"body": "Statt zu warten, melden Sie sich mit Ihrem ProConnect-Konto an.",
|
||||
"button": {
|
||||
"ariaLabel": "",
|
||||
"label": ""
|
||||
"ariaLabel": "Hinweis schließen",
|
||||
"label": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
{
|
||||
"createMeeting": "",
|
||||
"heading": "",
|
||||
"intro": "",
|
||||
"joinInputError": "",
|
||||
"joinInputExample": "",
|
||||
"joinInputLabel": "",
|
||||
"joinInputSubmit": "",
|
||||
"joinMeeting": "",
|
||||
"joinMeetingTipContent": "",
|
||||
"joinMeetingTipHeading": "",
|
||||
"loginToCreateMeeting": "",
|
||||
"moreLinkLabel": "",
|
||||
"moreLink": "",
|
||||
"moreAbout": "",
|
||||
"createMeeting": "Meeting erstellen",
|
||||
"heading": "Einfache und sichere Videokonferenzen",
|
||||
"intro": "Kommunizieren und arbeiten Sie mühelos, ohne Ihre digitale Souveränität zu gefährden",
|
||||
"joinInputError": "Verwenden Sie einen Meeting-Link oder -Code. Beispiele:",
|
||||
"joinInputExample": "URL oder 10-stelliger Code",
|
||||
"joinInputLabel": "Meeting-Link",
|
||||
"joinInputSubmit": "Meeting beitreten",
|
||||
"joinMeeting": "Meeting beitreten",
|
||||
"joinMeetingTipContent": "Sie können einem Meeting beitreten, indem Sie den vollständigen Link in die Adressleiste Ihres Browsers einfügen.",
|
||||
"joinMeetingTipHeading": "Wussten Sie schon?",
|
||||
"loginToCreateMeeting": "Melden Sie sich an, um ein Meeting zu erstellen",
|
||||
"moreLinkLabel": "Mehr erfahren – neues Tab",
|
||||
"moreLink": "Mehr erfahren",
|
||||
"moreAbout": "über Visio",
|
||||
"createMenu": {
|
||||
"laterOption": "",
|
||||
"instantOption": ""
|
||||
"laterOption": "Meeting für später planen",
|
||||
"instantOption": "Sofort-Meeting starten"
|
||||
},
|
||||
"laterMeetingDialog": {
|
||||
"heading": "",
|
||||
"description": "",
|
||||
"copy": "",
|
||||
"copied": "",
|
||||
"permissions": ""
|
||||
"heading": "Ihre Zugangsdaten",
|
||||
"description": "Senden Sie diesen Link an die Personen, die Sie zum Meeting einladen möchten. Sie können ohne ProConnect teilnehmen.",
|
||||
"copy": "Meeting-Link kopieren",
|
||||
"copied": "Link in die Zwischenablage kopiert",
|
||||
"permissions": "Personen mit diesem Link benötigen keine Genehmigung, um diesem Meeting beizutreten."
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
},
|
||||
"beta": {
|
||||
"text": "",
|
||||
"tooltip": ""
|
||||
"label": "Zurück",
|
||||
"tooltip": "Zurück"
|
||||
},
|
||||
"next": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
"label": "Weiter",
|
||||
"tooltip": "Weiter"
|
||||
},
|
||||
"beta": {
|
||||
"text": "An der Beta teilnehmen",
|
||||
"tooltip": "Formular ausfüllen"
|
||||
},
|
||||
"slide1": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"imgAlt": ""
|
||||
"title": "Testen Sie Visio für Ihre täglichen Aufgaben",
|
||||
"body": "Entdecken Sie eine intuitive und zugängliche Lösung, entwickelt für alle Mitarbeitenden im öffentlichen Dienst, ihre Partner und viele weitere.",
|
||||
"imgAlt": "Illustration einer benutzerfreundlichen und barrierefreien Kollaborationsplattform"
|
||||
},
|
||||
"slide2": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"imgAlt": ""
|
||||
"title": "Gruppenanrufe ohne Einschränkungen",
|
||||
"body": "Unbegrenzte Meeting-Dauer mit flüssiger, hochwertiger Kommunikation – unabhängig von der Gruppengröße.",
|
||||
"imgAlt": "Bild eines virtuellen Meetings mit mehreren nahtlos zusammenarbeitenden Teilnehmenden"
|
||||
},
|
||||
"slide3": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"imgAlt": ""
|
||||
"title": "Verwandeln Sie Ihre Meetings mit KI",
|
||||
"body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta – jetzt testen!",
|
||||
"imgAlt": "Illustration KI-gestützter Notizen in einem virtuellen Meeting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
{
|
||||
"title": "",
|
||||
"title": "Rechtliche Hinweise",
|
||||
"creator": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"title": "Herausgeber",
|
||||
"body": "Der Visio-Dienst wird herausgegeben von der Interministeriellen Digitaldirektion des Staates (DINUM) mit Sitz:",
|
||||
"contact": {
|
||||
"title": "",
|
||||
"address": "",
|
||||
"city": "",
|
||||
"phone": "",
|
||||
"siret": "",
|
||||
"siren": ""
|
||||
"title": "Kontaktdaten",
|
||||
"address": "20 avenue de Ségur",
|
||||
"city": "75007 Paris",
|
||||
"phone": "Tel. Zentrale: 01.71.21.01.70",
|
||||
"siret": "SIRET: 12000101100010 (Generalsekretariat der Regierung)",
|
||||
"siren": "SIREN: 120 001 011"
|
||||
}
|
||||
},
|
||||
"director": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Verantwortliche für die Veröffentlichung",
|
||||
"body": "Die Verantwortliche für die Veröffentlichung ist Frau Stéphanie Schaer, Interministerielle Direktorin für Digitalisierung."
|
||||
},
|
||||
"hosting": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Hosting",
|
||||
"body": "Der Dienst wird gehostet von Outscale West-1 CloudGouv SecNumCloud mit Standort in Frankreich."
|
||||
},
|
||||
"accessibility": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"more": "",
|
||||
"link": "",
|
||||
"status": ""
|
||||
"title": "Barrierefreiheit",
|
||||
"body": "Die Einhaltung der Standards für digitale Barrierefreiheit ist ein zukünftiges Ziel. Die Website wurde noch nicht auditiert. Das Team bemüht sich jedoch, eine für alle zugängliche Website zu erstellen, unter Berücksichtigung der Empfehlungen des RGAA.",
|
||||
"more": "Mehr erfahren: ",
|
||||
"link": "Link zur Seite über Barrierefreiheit",
|
||||
"status": "Barrierefreiheitsstatus: nicht konform"
|
||||
},
|
||||
"reuse": {
|
||||
"title": "",
|
||||
"body1": "",
|
||||
"license": "",
|
||||
"body2": "",
|
||||
"body3": ""
|
||||
"title": "Wiederverwendung von Inhalten und Links",
|
||||
"body1": "Sofern nicht ausdrücklich anders angegeben und geistige Eigentumsrechte Dritter betroffen sind, werden die Inhalte dieser Website unter der ",
|
||||
"license": "offenen Lizenz Etalab 2.0",
|
||||
"body2": "bereitgestellt. Insbesondere dürfen Sie diese Inhalte frei vervielfältigen, kopieren, ändern, extrahieren, umwandeln, verbreiten, weiterleiten, veröffentlichen, übertragen und nutzen – unter der Bedingung, dass die Quelle und das Datum der letzten Aktualisierung genannt werden und Dritte nicht durch die Informationen in die Irre geführt werden.",
|
||||
"body3": "Jede öffentliche oder private Website ist berechtigt, ohne vorherige Genehmigung, einen Link (einschließlich eines Deep Links) zu den auf dieser Website veröffentlichten Informationen zu setzen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
{
|
||||
"defaultName": "",
|
||||
"defaultName": "Ein Mitwirkender",
|
||||
"joined": {
|
||||
"description": ""
|
||||
"description": "{{name}} ist dem Raum beigetreten"
|
||||
},
|
||||
"raised": {
|
||||
"description": "",
|
||||
"cta": ""
|
||||
"description": "{{name}} hat die Hand gehoben.",
|
||||
"cta": "Warteliste öffnen"
|
||||
},
|
||||
"muted": "",
|
||||
"openChat": "",
|
||||
"muted": "{{name}} hat dein Mikrofon stummgeschaltet. Kein Teilnehmer kann dich hören.",
|
||||
"openChat": "Chat öffnen",
|
||||
"lowerHand": {
|
||||
"auto": "",
|
||||
"dismiss": ""
|
||||
"auto": "Es scheint, dass du angefangen hast zu sprechen, daher wird deine Hand gesenkt.",
|
||||
"dismiss": "Hand oben lassen"
|
||||
},
|
||||
"reaction": {
|
||||
"description": ""
|
||||
"description": "{{name}} hat mit {{emoji}} reagiert"
|
||||
},
|
||||
"waitingParticipants": {
|
||||
"one": "",
|
||||
"several": "",
|
||||
"open": "",
|
||||
"accept": ""
|
||||
"one": "Eine Person möchte diesem Anruf beitreten.",
|
||||
"several": "Mehrere Personen möchten diesem Anruf beitreten.",
|
||||
"open": "Öffnen",
|
||||
"accept": "Annehmen"
|
||||
},
|
||||
"transcript": {
|
||||
"started": "",
|
||||
"stopped": ""
|
||||
"started": "{{name}} hat die Transkription des Treffens gestartet.",
|
||||
"stopped": "{{name}} hat die Transkription des Treffens gestoppt."
|
||||
},
|
||||
"screenRecording": {
|
||||
"started": "",
|
||||
"stopped": ""
|
||||
"started": "{{name}} hat die Aufzeichnung des Treffens gestartet.",
|
||||
"stopped": "{{name}} hat die Aufzeichnung des Treffens gestoppt."
|
||||
},
|
||||
"recordingSave": {
|
||||
"transcript": {
|
||||
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald die Transkription fertig ist.",
|
||||
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald die Transkription fertig ist."
|
||||
},
|
||||
"screenRecording": {
|
||||
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald sie fertig ist.",
|
||||
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald sie fertig ist."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"error": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Aufzeichnung nicht verfügbar",
|
||||
"body": "Diese Aufzeichnung konnte nicht gefunden werden oder wurde gelöscht."
|
||||
},
|
||||
"expired": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Aufzeichnung abgelaufen",
|
||||
"body": "Diese Aufzeichnung wurde am {{date}} gelöscht."
|
||||
},
|
||||
"authentication": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Authentifizierung erforderlich",
|
||||
"body": "Bitte melden Sie sich an, um auf diese Aufzeichnung zuzugreifen."
|
||||
},
|
||||
"unsaved": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Download nicht verfügbar",
|
||||
"body": "Diese Aufzeichnung ist noch nicht zum Herunterladen bereit. Bitte versuchen Sie es später erneut."
|
||||
},
|
||||
"success": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"expiration": "",
|
||||
"button": ""
|
||||
"title": "Ihre Aufzeichnung ist bereit!",
|
||||
"body": "Aufzeichnung des Treffens <b>{{room}}</b> vom {{created_at}}.",
|
||||
"expiration": "Achtung, diese Aufzeichnung wird nach {{expiration_days}} Tag(en) gelöscht.",
|
||||
"button": "Herunterladen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,145 @@
|
||||
{
|
||||
"feedback": {
|
||||
"heading": "",
|
||||
"home": "",
|
||||
"back": ""
|
||||
"heading": "Sie haben das Meeting verlassen",
|
||||
"home": "Zur Startseite zurückkehren",
|
||||
"back": "Dem Meeting erneut beitreten"
|
||||
},
|
||||
"join": {
|
||||
"videoinput": {
|
||||
"choose": "",
|
||||
"disable": "",
|
||||
"enable": "",
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
"choose": "Kamera auswählen",
|
||||
"disable": "Kamera deaktivieren",
|
||||
"enable": "Kamera aktivieren",
|
||||
"label": "Kamera",
|
||||
"placeholder": "Kamera aktivieren, um die Vorschau zu sehen"
|
||||
},
|
||||
"audioinput": {
|
||||
"choose": "",
|
||||
"disable": "",
|
||||
"enable": "",
|
||||
"label": ""
|
||||
"choose": "Mikrofon auswählen",
|
||||
"disable": "Mikrofon deaktivieren",
|
||||
"enable": "Mikrofon aktivieren",
|
||||
"label": "Mikrofon"
|
||||
},
|
||||
"heading": "",
|
||||
"effects": {
|
||||
"description": "",
|
||||
"title": "",
|
||||
"subTitle": ""
|
||||
"description": "Effekte anwenden",
|
||||
"title": "Effekte",
|
||||
"subTitle": "Konfigurieren Sie die Effekte Ihrer Kamera."
|
||||
},
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "",
|
||||
"toggleOff": "",
|
||||
"toggleOn": "",
|
||||
"usernameHint": "",
|
||||
"usernameLabel": "",
|
||||
"heading": "Dem Meeting beitreten?",
|
||||
"joinLabel": "Beitreten",
|
||||
"joinMeeting": "Meeting beitreten",
|
||||
"toggleOff": "Klicken, um auszuschalten",
|
||||
"toggleOn": "Klicken, um einzuschalten",
|
||||
"usernameHint": "Wird anderen Teilnehmern angezeigt",
|
||||
"usernameLabel": "Ihr Name",
|
||||
"errors": {
|
||||
"usernameEmpty": ""
|
||||
"usernameEmpty": "Ihr Name darf nicht leer sein"
|
||||
},
|
||||
"cameraDisabled": "",
|
||||
"cameraStarting": "",
|
||||
"cameraDisabled": "Kamera ist deaktiviert.",
|
||||
"cameraStarting": "Kamera wird gestartet.",
|
||||
"waiting": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Beitrittsanfrage wird gesendet...",
|
||||
"body": "Sie können diesem Anruf beitreten, sobald jemand Sie autorisiert"
|
||||
},
|
||||
"denied": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Sie können diesem Anruf nicht beitreten",
|
||||
"body": "Ihre Beitrittsanfrage wurde abgelehnt."
|
||||
},
|
||||
"timeoutInvite": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Sie können diesem Anruf nicht beitreten",
|
||||
"body": "Niemand hat auf Ihre Anfrage reagiert"
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "",
|
||||
"leaveRoomPrompt": "Dadurch verlassen Sie das Meeting.",
|
||||
"shareDialog": {
|
||||
"copy": "",
|
||||
"copied": "",
|
||||
"heading": "",
|
||||
"description": "",
|
||||
"permissions": ""
|
||||
"copy": "Meeting-Link kopieren",
|
||||
"copyButton": "Link kopieren",
|
||||
"copied": "Link in die Zwischenablage kopiert",
|
||||
"heading": "Ihr Meeting ist bereit",
|
||||
"description": "Teilen Sie diesen Link mit Personen, die Sie zum Meeting einladen möchten.",
|
||||
"permissions": "Personen mit diesem Link benötigen keine Erlaubnis, um diesem Meeting beizutreten."
|
||||
},
|
||||
"error": {
|
||||
"createRoom": {
|
||||
"heading": "",
|
||||
"body": ""
|
||||
"heading": "Authentifizierung erforderlich",
|
||||
"body": "Dieser Raum wurde noch nicht erstellt. Bitte authentifizieren Sie sich, um ihn zu erstellen, oder warten Sie, bis ein authentifizierter Benutzer dies tut."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "",
|
||||
"ariaLabel": "",
|
||||
"message": "",
|
||||
"macInstructions": "",
|
||||
"macSystemPreferences": "",
|
||||
"helpLinkText": "",
|
||||
"helpLinkLabel": "",
|
||||
"closeButton": "",
|
||||
"newTab": ""
|
||||
"title": "Bildschirmfreigabe nicht möglich",
|
||||
"ariaLabel": "Bildschirmfreigabe nicht möglich",
|
||||
"message": "Ihr Browser darf möglicherweise den Bildschirm Ihres Computers nicht aufzeichnen.",
|
||||
"macInstructions": "Gehen Sie zu Ihren",
|
||||
"macSystemPreferences": "Systemeinstellungen",
|
||||
"helpLinkText": "Weitere Informationen finden Sie unter",
|
||||
"helpLinkLabel": "Präsentationsproblem",
|
||||
"closeButton": "Schließen",
|
||||
"newTab": "Neues Fenster"
|
||||
}
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "",
|
||||
"camera": "",
|
||||
"shareScreen": "",
|
||||
"stopScreenShare": "",
|
||||
"microphone": "Mikrofon",
|
||||
"camera": "Kamera",
|
||||
"chat": {
|
||||
"open": "",
|
||||
"closed": "",
|
||||
"open": "Chat schließen",
|
||||
"closed": "Chat öffnen",
|
||||
"input": {
|
||||
"textArea": {
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
"label": "Nachricht eingeben",
|
||||
"placeholder": "Nachricht eingeben"
|
||||
},
|
||||
"button": {
|
||||
"label": ""
|
||||
"label": "Nachricht senden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"open": "",
|
||||
"closed": ""
|
||||
"open": "Informationen ausblenden",
|
||||
"closed": "Informationen anzeigen"
|
||||
},
|
||||
"hand": {
|
||||
"raise": "",
|
||||
"lower": ""
|
||||
"raise": "Hand heben",
|
||||
"lower": "Hand senken"
|
||||
},
|
||||
"screenShare": {
|
||||
"start": "",
|
||||
"stop": ""
|
||||
"start": "Bildschirm freigeben",
|
||||
"stop": "Bildschirmfreigabe beenden"
|
||||
},
|
||||
"leave": "",
|
||||
"leave": "Verlassen",
|
||||
"participants": {
|
||||
"open": "",
|
||||
"closed": ""
|
||||
"open": "Alle ausblenden",
|
||||
"closed": "Alle anzeigen"
|
||||
},
|
||||
"tools": {
|
||||
"open": "",
|
||||
"closed": ""
|
||||
"open": "Weitere Tools ausblenden",
|
||||
"closed": "Weitere Tools anzeigen"
|
||||
},
|
||||
"admin": {
|
||||
"open": "",
|
||||
"closed": ""
|
||||
"open": "Admin ausblenden",
|
||||
"closed": "Admin öffnen"
|
||||
},
|
||||
"moreOptions": "",
|
||||
"moreOptions": "Weitere Optionen",
|
||||
"reactions": {
|
||||
"button": "",
|
||||
"send": "",
|
||||
"you": ""
|
||||
"button": "Reaktion senden",
|
||||
"send": "Reaktion {{emoji}} senden",
|
||||
"you": "Sie"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "",
|
||||
"buttonLabel": "Weitere Optionen",
|
||||
"items": {
|
||||
"feedback": "",
|
||||
"settings": "",
|
||||
"username": "",
|
||||
"effects": "",
|
||||
"switchCamera": "",
|
||||
"feedback": "Feedback geben",
|
||||
"settings": "Einstellungen",
|
||||
"username": "Ihren Namen aktualisieren",
|
||||
"effects": "Effekte anwenden",
|
||||
"switchCamera": "Kamera wechseln",
|
||||
"fullscreen": {
|
||||
"enter": "",
|
||||
"exit": ""
|
||||
"enter": "Vollbild",
|
||||
"exit": "Vollbildmodus verlassen"
|
||||
},
|
||||
"support": ""
|
||||
"support": "Support kontaktieren"
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
"activateCamera": "Ihre Kamera ist deaktiviert. Wählen Sie eine Option, um sie zu aktivieren.",
|
||||
"notAvailable": "Videoeffekte werden in Kürze in Ihrem Browser verfügbar sein. Wir arbeiten daran! In der Zwischenzeit können Sie Google Chrome für beste Leistung oder Firefox verwenden :(",
|
||||
"notAvailable": "Videoeffekte werden bald in Ihrem Browser verfügbar sein. Wir arbeiten daran! In der Zwischenzeit können Sie Google Chrome für die beste Leistung oder Firefox verwenden :(",
|
||||
"heading": "Unschärfe",
|
||||
"blur": {
|
||||
"title": "Hintergrundunschärfe",
|
||||
@@ -161,226 +160,237 @@
|
||||
"clear": "Brille entfernen"
|
||||
},
|
||||
"french": {
|
||||
"apply": "Französische Touch hinzufügen",
|
||||
"clear": "Französische Touch entfernen"
|
||||
"apply": "Französischen Touch hinzufügen",
|
||||
"clear": "Französischen Touch entfernen"
|
||||
}
|
||||
},
|
||||
"experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität."
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"heading": {
|
||||
"participants": "",
|
||||
"effects": "",
|
||||
"chat": "",
|
||||
"transcript": "",
|
||||
"screenRecording": "",
|
||||
"admin": "",
|
||||
"tools": "",
|
||||
"info": ""
|
||||
"participants": "Teilnehmer",
|
||||
"effects": "Effekte",
|
||||
"chat": "Nachrichten im Chat",
|
||||
"transcript": "Transkription",
|
||||
"screenRecording": "Aufzeichnung",
|
||||
"admin": "Admin-Einstellungen",
|
||||
"tools": "Weitere Tools",
|
||||
"info": "Meeting-Informationen"
|
||||
},
|
||||
"content": {
|
||||
"participants": "",
|
||||
"effects": "",
|
||||
"chat": "",
|
||||
"transcript": "",
|
||||
"screenRecording": "",
|
||||
"admin": "",
|
||||
"tools": "",
|
||||
"info": ""
|
||||
"participants": "Teilnehmer",
|
||||
"effects": "Effekte",
|
||||
"chat": "Nachrichten",
|
||||
"transcript": "Transkription",
|
||||
"screenRecording": "Aufzeichnung",
|
||||
"admin": "Admin-Einstellungen",
|
||||
"tools": "Weitere Tools",
|
||||
"info": "Meeting-Informationen"
|
||||
},
|
||||
"closeButton": ""
|
||||
"closeButton": "{{content}} ausblenden"
|
||||
},
|
||||
"chat": {
|
||||
"disclaimer": ""
|
||||
"disclaimer": "Die Nachrichten sind nur für Teilnehmer zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Anrufs gelöscht."
|
||||
},
|
||||
"moreTools": {
|
||||
"body": "",
|
||||
"moreLink": "",
|
||||
"body": "Greifen Sie auf weitere Tools in Visio zu, um Ihre Meetings zu verbessern,",
|
||||
"moreLink": "mehr erfahren",
|
||||
"tools": {
|
||||
"transcript": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Transkription",
|
||||
"body": "Führen Sie ein schriftliches Protokoll Ihres Meetings."
|
||||
},
|
||||
"screenRecording": {
|
||||
"title": "",
|
||||
"body": ""
|
||||
"title": "Aufzeichnung",
|
||||
"body": "Zeichnen Sie Ihr Meeting auf, um es später erneut anzusehen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"roomInformation": {
|
||||
"title": "",
|
||||
"title": "Verbindungsinformationen",
|
||||
"button": {
|
||||
"ariaLabel": "",
|
||||
"copy": "",
|
||||
"copied": ""
|
||||
"ariaLabel": "Kopiere deine Meeting-Adresse",
|
||||
"copy": "Adresse kopieren",
|
||||
"copied": "Adresse kopiert"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transcript": {
|
||||
"start": {
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": "",
|
||||
"linkMore": ""
|
||||
"heading": "Dieses Gespräch transkribieren",
|
||||
"body": "Dieses Gespräch automatisch transkribieren und die Zusammenfassung in Docs erhalten.",
|
||||
"button": "Transkription starten",
|
||||
"loading": "Transkription wird gestartet",
|
||||
"linkMore": "Mehr erfahren"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Transkription (Beta) starten.",
|
||||
"linkMore": "Mehr erfahren"
|
||||
},
|
||||
"stop": {
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": ""
|
||||
"heading": "Transkription läuft...",
|
||||
"body": "Die Transkription deines Meetings läuft. Du erhältst das Ergebnis per E-Mail, sobald das Meeting beendet ist.",
|
||||
"button": "Transkription stoppen"
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "",
|
||||
"body": ""
|
||||
"heading": "Daten werden gespeichert…",
|
||||
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
|
||||
},
|
||||
"beta": {
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": ""
|
||||
"heading": "Werde Beta-Tester",
|
||||
"body": "Zeichne dein Meeting auf. Du erhältst eine Zusammenfassung per E-Mail nach dem Meeting.",
|
||||
"button": "Anmelden"
|
||||
},
|
||||
"alert": {
|
||||
"title": "",
|
||||
"title": "Transkription fehlgeschlagen",
|
||||
"body": {
|
||||
"stop": "",
|
||||
"start": ""
|
||||
"stop": "Die Transkription konnte nicht gestoppt werden. Bitte versuche es in einem Moment erneut.",
|
||||
"start": "Die Transkription konnte nicht gestartet werden. Bitte versuche es in einem Moment erneut."
|
||||
},
|
||||
"button": ""
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
"start": {
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": "",
|
||||
"linkMore": ""
|
||||
"heading": "Dieses Gespräch aufzeichnen",
|
||||
"body": "Zeichne dieses Gespräch auf, um es später anzusehen. Du erhältst die Videoaufnahme per E-Mail.",
|
||||
"button": "Aufzeichnung starten",
|
||||
"loading": "Aufzeichnung wird gestartet",
|
||||
"linkMore": "Mehr erfahren"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine videoaufnahme (Beta) starten.",
|
||||
"linkMore": "Mehr erfahren"
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "",
|
||||
"body": ""
|
||||
"heading": "Daten werden gespeichert…",
|
||||
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": ""
|
||||
"heading": "Aufzeichnung läuft…",
|
||||
"body": "Du erhältst das Ergebnis per E-Mail, sobald die Aufzeichnung abgeschlossen ist.",
|
||||
"button": "Aufzeichnung stoppen"
|
||||
},
|
||||
"alert": {
|
||||
"title": "",
|
||||
"title": "Aufzeichnung fehlgeschlagen",
|
||||
"body": {
|
||||
"stop": "",
|
||||
"start": ""
|
||||
"stop": "Die Aufzeichnung konnte nicht gestoppt werden. Bitte versuche es in einem Moment erneut.",
|
||||
"start": "Die Aufzeichnung konnte nicht gestartet werden. Bitte versuche es in einem Moment erneut."
|
||||
},
|
||||
"button": ""
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"description": "",
|
||||
"description": "Diese Einstellungen für Organisatoren ermöglichen dir die Kontrolle über dein Meeting. Nur Organisatoren haben Zugriff auf diese Optionen.",
|
||||
"access": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"type": "",
|
||||
"title": "Raumzugang",
|
||||
"description": "Diese Einstellungen gelten auch für zukünftige Ausgaben dieses Meetings.",
|
||||
"type": "Zugangsarten für Meetings",
|
||||
"levels": {
|
||||
"public": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Offen",
|
||||
"description": "Niemand muss um Zugang zum Meeting bitten."
|
||||
},
|
||||
"trusted": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Offen für vertrauenswürdige Personen",
|
||||
"description": "Authentifizierte Personen müssen nicht um Zugang bitten."
|
||||
},
|
||||
"restricted": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Eingeschränkt",
|
||||
"description": "Nicht eingeladene Personen müssen um Zugang bitten."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rating": {
|
||||
"submit": "",
|
||||
"question": "",
|
||||
"submit": "Absenden",
|
||||
"question": "Wie bewertest du die Qualität deines Gesprächs?",
|
||||
"levels": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
"min": "sehr schlecht",
|
||||
"max": "ausgezeichnet"
|
||||
}
|
||||
},
|
||||
"openFeedback": {
|
||||
"question": "",
|
||||
"placeholder": "",
|
||||
"submit": "",
|
||||
"skip": ""
|
||||
"question": "Was können wir tun, um Visio zu verbessern?",
|
||||
"placeholder": "Beschreibe deine Fehler oder teile deine Vorschläge…",
|
||||
"submit": "Absenden",
|
||||
"skip": "Überspringen"
|
||||
},
|
||||
"confirmationMessage": {
|
||||
"heading": "",
|
||||
"body": ""
|
||||
"heading": "Vielen Dank für deine Rückmeldung",
|
||||
"body": "Unser Produktteam nimmt sich Zeit, dein Feedback sorgfältig zu prüfen. Wir melden uns so bald wie möglich bei dir."
|
||||
},
|
||||
"authenticationMessage": {
|
||||
"heading": "",
|
||||
"placeholder": "",
|
||||
"submit": "",
|
||||
"ignore": ""
|
||||
"heading": "Wie können wir dich kontaktieren?",
|
||||
"placeholder": "Deine E-Mail-Adresse",
|
||||
"submit": "Senden",
|
||||
"ignore": "Ignorieren"
|
||||
},
|
||||
"participants": {
|
||||
"subheading": "",
|
||||
"contributors": "",
|
||||
"subheading": "Im Raum",
|
||||
"you": "Du",
|
||||
"contributors": "Mitwirkende",
|
||||
"collapsable": {
|
||||
"open": "",
|
||||
"close": ""
|
||||
"open": "{{name}}-Liste öffnen",
|
||||
"close": "{{name}}-Liste schließen"
|
||||
},
|
||||
"you": "",
|
||||
"muteYourself": "",
|
||||
"muteParticipant": "",
|
||||
"muteYourself": "Mikrofon ausschalten",
|
||||
"muteParticipant": "{{name}}’s Mikrofon ausschalten",
|
||||
"muteParticipantAlert": {
|
||||
"heading": "",
|
||||
"description": "",
|
||||
"confirm": "",
|
||||
"cancel": ""
|
||||
"heading": "{{name}} stummschalten",
|
||||
"description": "{{name}} für alle Teilnehmer stummschalten? {{name}} kann sich nur selbst wieder einschalten.",
|
||||
"confirm": "Stummschalten",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"raisedHands": "",
|
||||
"lowerParticipantHand": "",
|
||||
"lowerParticipantsHand": "",
|
||||
"raisedHands": "Meldungen",
|
||||
"lowerParticipantHand": "{{name}}’s Hand senken",
|
||||
"lowerParticipantsHand": "Alle Hände senken",
|
||||
"waiting": {
|
||||
"title": "",
|
||||
"title": "Warteraum",
|
||||
"accept": {
|
||||
"button": "",
|
||||
"label": "",
|
||||
"all": ""
|
||||
"button": "Zulassen",
|
||||
"label": "{{name}} zum Meeting zulassen",
|
||||
"all": "Alle zulassen"
|
||||
},
|
||||
"deny": {
|
||||
"button": "",
|
||||
"label": "",
|
||||
"all": ""
|
||||
"button": "Ablehnen",
|
||||
"label": "{{name}} vom Meeting ablehnen",
|
||||
"all": "Alle ablehnen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordingBadge": {
|
||||
"recordingStateToast": {
|
||||
"transcript": {
|
||||
"started": "",
|
||||
"starting": "",
|
||||
"stopping": ""
|
||||
"started": "Transkription läuft",
|
||||
"starting": "Transkription wird gestartet",
|
||||
"stopping": "Transkription wird gestoppt"
|
||||
},
|
||||
"screenRecording": {
|
||||
"started": "",
|
||||
"starting": "",
|
||||
"stopping": ""
|
||||
"started": "Aufzeichnung läuft",
|
||||
"starting": "Aufzeichnung wird gestartet",
|
||||
"stopping": "Aufzeichnung wird gestoppt"
|
||||
},
|
||||
"any": {
|
||||
"started": ""
|
||||
"started": "Aufzeichnung läuft"
|
||||
}
|
||||
},
|
||||
"participantTileFocus": {
|
||||
"pin": {
|
||||
"enable": "",
|
||||
"disable": ""
|
||||
"enable": "Anheften",
|
||||
"disable": "Lösen"
|
||||
},
|
||||
"effects": "",
|
||||
"muteParticipant": "",
|
||||
"fullScreen": ""
|
||||
"effects": "Visuelle Effekte anwenden",
|
||||
"muteParticipant": "{{name}} stummschalten",
|
||||
"fullScreen": "Vollbild"
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "",
|
||||
"stop": "",
|
||||
"ignore": ""
|
||||
"message": "Um eine Endlosschleife zu vermeiden, teile nicht deinen gesamten Bildschirm. Teile stattdessen einen Tab oder ein anderes Fenster.",
|
||||
"stop": "Präsentation beenden",
|
||||
"ignore": "Ignorieren"
|
||||
},
|
||||
"participantTile": {
|
||||
"screenShare": ""
|
||||
"screenShare": "{{name}}’s Bildschirm"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"createMeeting": {
|
||||
"createButton": "",
|
||||
"joinButton": "",
|
||||
"copyLinkTooltip": "",
|
||||
"resetLabel": "",
|
||||
"participantLimit": "",
|
||||
"popupBlocked": ""
|
||||
"createButton": "Erstellen",
|
||||
"joinButton": "Mit Visio beitreten",
|
||||
"copyLinkTooltip": "Link kopieren",
|
||||
"resetLabel": "Zurücksetzen",
|
||||
"participantLimit": "Bis zu 150 Teilnehmer.",
|
||||
"popupBlocked": "Popup wurde blockiert. Bitte erlauben Sie Popups für diese Website."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
{
|
||||
"account": {
|
||||
"currentlyLoggedAs": "",
|
||||
"heading": "",
|
||||
"youAreNotLoggedIn": "",
|
||||
"nameLabel": "",
|
||||
"authentication": ""
|
||||
"currentlyLoggedAs": "Sie sind derzeit angemeldet als <0>{{user}}</0>",
|
||||
"heading": "Konto",
|
||||
"youAreNotLoggedIn": "Sie sind nicht angemeldet.",
|
||||
"nameLabel": "Ihr Name",
|
||||
"authentication": "Authentifizierung"
|
||||
},
|
||||
"audio": {
|
||||
"microphone": {
|
||||
"heading": "",
|
||||
"label": ""
|
||||
"heading": "Mikrofon",
|
||||
"label": "Wählen Sie Ihre Audioeingabe"
|
||||
},
|
||||
"speakers": {
|
||||
"heading": "",
|
||||
"label": "",
|
||||
"test": "",
|
||||
"ongoingTest": ""
|
||||
"heading": "Lautsprecher",
|
||||
"label": "Wählen Sie Ihre Audioausgabe",
|
||||
"test": "Testen",
|
||||
"ongoingTest": "Soundtest läuft…"
|
||||
},
|
||||
"permissionsRequired": ""
|
||||
"permissionsRequired": "Berechtigungen erforderlich"
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "",
|
||||
"label": "",
|
||||
"heading": "Tonbenachrichtigungen",
|
||||
"label": "Tonbenachrichtigungen für",
|
||||
"actions": {
|
||||
"disable": "",
|
||||
"enable": ""
|
||||
"disable": "Deaktivieren",
|
||||
"enable": "Aktivieren"
|
||||
},
|
||||
"items": {
|
||||
"participantJoined": "",
|
||||
"handRaised": "",
|
||||
"messageReceived": ""
|
||||
"participantJoined": "Teilnehmer beigetreten",
|
||||
"handRaised": "Hand gehoben",
|
||||
"messageReceived": "Nachricht erhalten"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"heading": ""
|
||||
"heading": "Einstellungen"
|
||||
},
|
||||
"language": {
|
||||
"heading": "",
|
||||
"label": ""
|
||||
"heading": "Sprache",
|
||||
"label": "Sprache"
|
||||
},
|
||||
"settingsButtonLabel": "",
|
||||
"settingsButtonLabel": "Einstellungen",
|
||||
"tabs": {
|
||||
"account": "",
|
||||
"audio": "",
|
||||
"general": "",
|
||||
"notifications": ""
|
||||
"account": "Profil",
|
||||
"audio": "Audio",
|
||||
"general": "Allgemein",
|
||||
"notifications": "Benachrichtigungen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,94 @@
|
||||
{
|
||||
"title": "",
|
||||
"title": "Nutzungsbedingungen für Visio",
|
||||
"articles": {
|
||||
"article1": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"title": "1. Geltungsbereich",
|
||||
"content": "Dieses Dokument definiert die Nutzungsbedingungen für Visio, den Videokonferenzdienst der Verwaltung."
|
||||
},
|
||||
"article2": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"purposes": ""
|
||||
"title": "2. Zweck der Plattform",
|
||||
"content": "Der Dienst fördert die Zusammenarbeit im Team und das Arbeiten aus der Ferne und erleichtert die Organisation von Besprechungen, Konferenzen oder Schulungen.",
|
||||
"purposes": "Visio wird von DINUM entwickelt und betrieben. Jede Nutzung des Dienstes muss mit diesen Nutzungsbedingungen übereinstimmen."
|
||||
},
|
||||
"article3": {
|
||||
"title": "",
|
||||
"definition": ""
|
||||
"title": "3. Zugang zum Dienst",
|
||||
"definition": "Visio wird öffentlichen Bediensteten zur Verfügung gestellt, die über ProConnect verbunden sind, um einen Videokonferenzraum zu verwalten. Der verbundene öffentliche Bedienstete kann dann einen Link übermitteln, der den Zugang zum Raum unter seiner Verantwortung ermöglicht."
|
||||
},
|
||||
"article4": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"title": "4. Nutzung",
|
||||
"content": "Die Nutzung des Dienstes ist kostenlos. Um einen Videokonferenzraum zu verwalten, ist eine Verbindung über ProConnect erforderlich."
|
||||
},
|
||||
"article5": {
|
||||
"title": "",
|
||||
"title": "5. Funktionen",
|
||||
"sections": {
|
||||
"section1": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"paragraph1": "",
|
||||
"paragraph2": "",
|
||||
"capabilities": ["", "", "", ""],
|
||||
"paragraph3": ""
|
||||
"title": "5.1. Funktionen für öffentliche Bedienstete",
|
||||
"content": "Visio bietet öffentlichen Bediensteten die Möglichkeit, von einem Standardarbeitsplatz aus einen Videokonferenzraum zu erstellen, der mit ihrem ProConnect-Konto verknüpft ist.",
|
||||
"paragraph1": "Jeder Staatsbedienstete, dessen Tätigkeiten mit der Nutzung von Visio vereinbar sind oder der keinen spezifischen Vertraulichkeitsverpflichtungen oder dem Berufsgeheimnis unterliegt, kann eine Videokonferenz erstellen. Er kann dann den Besprechungslink mit anderen Teilnehmern teilen, unabhängig davon, ob sie Teil der Verwaltung sind oder nicht.",
|
||||
"paragraph2": "Zu diesem Zweck kann jeder Raumorganisator:",
|
||||
"capabilities": [
|
||||
"Den Zugang zum Raum mit einem Warteraum schützen;",
|
||||
"Den Eintritt jedes Teilnehmers in den Raum autorisieren;",
|
||||
"Die Mikrofone und Kameras der Teilnehmer stummschalten;",
|
||||
"Einen Teilnehmer aus dem Raum entfernen."
|
||||
],
|
||||
"paragraph3": "Nur Bedienstete, deren Tätigkeitsbereich zur Nutzung von Visio berechtigt ist oder die <u>keinen spezifischen Vertraulichkeitsverpflichtungen oder dem Berufsgeheimnis unterliegen</u>, dürfen diesen Dienst und seine Funktionen nutzen oder kontrollieren."
|
||||
},
|
||||
"section2": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"paragraph": "",
|
||||
"capabilities": ["", "", "", "", "", "", ""]
|
||||
"title": "5.2. Funktionen für Nutzer",
|
||||
"content": "Visio ermöglicht es jedem Nutzer, ob öffentlicher Bediensteter oder nicht, an jedem Visio-Raum teilzunehmen, zu dem er von einem Staatsbediensteten eingeladen wurde, und alle Funktionen zu nutzen.",
|
||||
"paragraph": "Zu diesem Zweck kann jeder Teilnehmer:",
|
||||
"capabilities": [
|
||||
"Seine angezeigte Identität beim Betreten des Raums festlegen;",
|
||||
"Über Audio und/oder Video aus der Ferne kommunizieren;",
|
||||
"Seinen Bildschirm teilen;",
|
||||
"Während der Sitzung öffentlich mit allen Nachrichten austauschen;",
|
||||
"Informationen über die Konferenz einsehen;",
|
||||
"Seine Kamera und/oder sein Mikrofon jederzeit ein- oder ausschalten;",
|
||||
"Die Privatsphäre seines Arbeitsplatzes schützen, indem er vor dem Betreten des Raums oder sogar während der Videokonferenz einen Hintergrund auswählt und aktiviert."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"article6": {
|
||||
"title": "",
|
||||
"title": "6. Verpflichtungen und Verantwortlichkeiten der Nutzer",
|
||||
"sections": {
|
||||
"section1": {
|
||||
"title": "",
|
||||
"paragraphs": ["", ""]
|
||||
"title": "6.1. Konforme Nutzung",
|
||||
"paragraphs": [
|
||||
"Der Dienst wird öffentlichen Bediensteten über ProConnect zur Verfügung gestellt.",
|
||||
"Der Nutzer ist für die Daten oder Inhalte verantwortlich, die er im Nachrichtendienst des Videokonferenzraums eingibt. Er muss sicherstellen, dass er nur angemessene Nachrichten eingibt."
|
||||
]
|
||||
},
|
||||
"section2": {
|
||||
"title": "",
|
||||
"paragraphs": [""]
|
||||
"title": "6.2. Verbotene Nutzung",
|
||||
"paragraphs": [
|
||||
"Der Nutzer verpflichtet sich, keine Inhalte oder Informationen in den Nachrichtendienst des Visio-Raums einzugeben, die gegen geltende gesetzliche und regulatorische Bestimmungen verstoßen."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"article7": {
|
||||
"title": "",
|
||||
"title": "7. Verpflichtungen und Verantwortlichkeiten von DINUM",
|
||||
"sections": {
|
||||
"section1": {
|
||||
"title": "",
|
||||
"content": "",
|
||||
"paragraphs": ["", "", ""]
|
||||
"title": "7.1. Sicherheit und Plattformzugang",
|
||||
"content": "DINUM verpflichtet sich, Visio zu sichern, insbesondere durch alle notwendigen Maßnahmen zur Gewährleistung der Sicherheit und Vertraulichkeit der bereitgestellten Informationen.",
|
||||
"paragraphs": [
|
||||
"DINUM verpflichtet sich, die notwendigen und angemessenen Mittel bereitzustellen, um einen kontinuierlichen Zugang zu Visio zu gewährleisten.",
|
||||
"DINUM behält sich das Recht vor, den Dienst aus Wartungsgründen oder aus anderen als notwendig erachteten Gründen ohne Vorankündigung zu ändern, zu modifizieren oder auszusetzen.",
|
||||
"DINUM behält sich das Recht vor, ein Nutzerkonto des Dienstes zu sperren oder zu löschen, das gegen diese Nutzungsbedingungen verstoßen hat, unbeschadet etwaiger strafrechtlicher und zivilrechtlicher Maßnahmen, die gegen den Nutzer ergriffen werden könnten."
|
||||
]
|
||||
},
|
||||
"section2": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"title": "7.2. Open Source und Lizenzen",
|
||||
"content": "Der Quellcode von Visio ist frei und hier verfügbar: https://github.com/suitenumerique/meet\nDie von DINUM angebotenen Inhalte stehen unter einer Open License, mit Ausnahme von Logos sowie ikonografischen und fotografischen Darstellungen, die möglicherweise eigenen Lizenzen unterliegen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"article8": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"title": "8. Änderung der Nutzungsbedingungen",
|
||||
"content": "Die Bedingungen dieser Nutzungsbedingungen können jederzeit ohne Vorankündigung geändert oder ergänzt werden, abhängig von Änderungen am Dienst, Gesetzesänderungen oder aus anderen als notwendig erachteten Gründen.\nDiese Änderungen und Aktualisierungen sind für den Nutzer verbindlich, der daher regelmäßig diesen Abschnitt konsultieren muss, um die geltenden Bedingungen zu überprüfen."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,15 @@
|
||||
"screenRecording": {
|
||||
"started": "{{name}} started the meeting recording.",
|
||||
"stopped": "{{name}} stopped the meeting recording."
|
||||
},
|
||||
"recordingSave": {
|
||||
"transcript": {
|
||||
"message": "Your recording is being saved! We’ll send the transcript to <strong>{{email}}</strong> as soon as it’s ready.",
|
||||
"default": "Your recording is being saved! We’ll send the transcript to your email as soon as it’s ready."
|
||||
},
|
||||
"screenRecording": {
|
||||
"message": "Your recording is being saved! We’ll send a notification to <strong>{{email}}</strong> as soon as it’s ready.",
|
||||
"default": "Your recording is being saved! We’ll send a notification to your email as soon as it’s ready."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +163,7 @@
|
||||
"apply": "Add French touch",
|
||||
"clear": "Remove French touch"
|
||||
}
|
||||
},
|
||||
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"heading": {
|
||||
@@ -224,6 +223,11 @@
|
||||
"loading": "Transcription starting",
|
||||
"linkMore": "Learn more"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Restricted Access",
|
||||
"body": "For security reasons, only the meeting creator or an admin can start a transcription (beta).",
|
||||
"linkMore": "Learn more"
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Transcription in progress...",
|
||||
"body": "The transcription of your meeting is in progress. You will receive the result by email once the meeting is finished.",
|
||||
@@ -231,7 +235,7 @@
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Saving your data…",
|
||||
"body": "This process may take a few minutes. Thank you for your patience."
|
||||
"body": "You can leave the meeting if you wish; the recording will finish automatically."
|
||||
},
|
||||
"beta": {
|
||||
"heading": "Become a beta tester",
|
||||
@@ -255,9 +259,14 @@
|
||||
"loading": "Recording starting",
|
||||
"linkMore": "Learn more"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Restricted Access",
|
||||
"body": "For security reasons, only the meeting creator or an admin can start a recording (beta).",
|
||||
"linkMore": "Learn more"
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Saving your data…",
|
||||
"body": "This process may take a few minutes. Thank you for your patience."
|
||||
"body": "You can leave the meeting if you wish; the recording will finish automatically."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Recording in progress…",
|
||||
|
||||
@@ -29,5 +29,15 @@
|
||||
"screenRecording": {
|
||||
"started": "{{name}} a démarré l'enregistrement de la réunion.",
|
||||
"stopped": "{{name}} a arrêté l'enregistrement de la réunion."
|
||||
},
|
||||
"recordingSave": {
|
||||
"transcript": {
|
||||
"message": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail à <strong>{{email}}</strong> dès que la transcription sera prête.",
|
||||
"default": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail dès que la transcription sera prête."
|
||||
},
|
||||
"screenRecording": {
|
||||
"message": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail à <strong>{{email}}</strong> dès qu’il sera prêt.",
|
||||
"default": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail dès qu’il sera prêt."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"heading": "Rejoindre la réunion ?",
|
||||
"effects": {
|
||||
"description": "Appliquer des effets",
|
||||
"description": "Effets d'arrière plan",
|
||||
"title": "Effets",
|
||||
"subTitle": "Paramétrez les effets de votre caméra."
|
||||
},
|
||||
@@ -128,7 +128,7 @@
|
||||
"feedback": "Partager votre avis",
|
||||
"settings": "Paramètres",
|
||||
"username": "Choisir votre nom",
|
||||
"effects": "Appliquer des effets",
|
||||
"effects": "Effets d'arrière plan",
|
||||
"switchCamera": "Changer de caméra",
|
||||
"fullscreen": {
|
||||
"enter": "Plein écran",
|
||||
@@ -163,8 +163,7 @@
|
||||
"apply": "Ajouter la touche française",
|
||||
"clear": "Retirer la touche française"
|
||||
}
|
||||
},
|
||||
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité."
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"heading": {
|
||||
@@ -224,6 +223,11 @@
|
||||
"loading": "Démarrage de la transcription",
|
||||
"linkMore": "En savoir plus"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Accès restreint",
|
||||
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer une transcription (beta).",
|
||||
"linkMore": "En savoir plus"
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Transcription en cours …",
|
||||
"body": "La transcription de votre réunion est en cours. Vous recevrez le resultat par email une fois la réunion terminée.",
|
||||
@@ -231,7 +235,7 @@
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Sauvegarde de vos données…",
|
||||
"body": "Cette opération peut durer quelques minutes. Merci de votre patience."
|
||||
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
|
||||
},
|
||||
"beta": {
|
||||
"heading": "Devenez beta testeur",
|
||||
@@ -255,9 +259,14 @@
|
||||
"loading": "Démarrage de l'enregistrement",
|
||||
"linkMore": "En savoir plus"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Accès restreint",
|
||||
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer un enregistrement (beta).",
|
||||
"linkMore": "En savoir plus"
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Sauvegarde de vos données…",
|
||||
"body": "Cette opération peut durer quelques minutes. Merci de votre patience."
|
||||
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Enregistrement en cours …",
|
||||
@@ -372,7 +381,7 @@
|
||||
"enable": "Épingler",
|
||||
"disable": "Annuler l'épinglage"
|
||||
},
|
||||
"effects": "Appliquer des effets",
|
||||
"effects": "Effets d'arrière plan",
|
||||
"muteParticipant": "Couper le micro de {{name}}",
|
||||
"fullScreen": "Plein écran"
|
||||
},
|
||||
|
||||
@@ -29,5 +29,15 @@
|
||||
"screenRecording": {
|
||||
"started": "{{name}} is begonnen met het opnemen van de vergadering.",
|
||||
"stopped": "{{name}} is gestopt met het opnemen van de vergadering."
|
||||
},
|
||||
"recordingSave": {
|
||||
"transcript": {
|
||||
"message": "We zijn uw opname aan het voltooien! U ontvangt een e-mail op <strong>{{email}}</strong> zodra de transcriptie klaar is.",
|
||||
"default": "We zijn uw opname aan het voltooien! U ontvangt een e-mail zodra de transcriptie klaar is."
|
||||
},
|
||||
"screenRecording": {
|
||||
"message": "We zijn uw opname aan het voltooien! U ontvangt een e-mail op <strong>{{email}}</strong> zodra deze klaar is.",
|
||||
"default": "We zijn uw opname aan het voltooien! U ontvangt een e-mail zodra deze klaar is."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +163,7 @@
|
||||
"apply": "Franse stijl toevoegen",
|
||||
"clear": "Franse stijl verwijderen"
|
||||
}
|
||||
},
|
||||
"experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"heading": {
|
||||
@@ -224,6 +223,11 @@
|
||||
"loading": "Transcriptie begint",
|
||||
"linkMore": "Meer informatie"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Toegang beperkt",
|
||||
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een transcriptie starten (beta).",
|
||||
"linkMore": "Meer informatie"
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Transcriptie bezig...",
|
||||
"body": "De transcriptie van uw vergadering is bezig. U ontvangt het resultaat per e-mail zodra de vergadering is afgelopen.",
|
||||
@@ -231,7 +235,7 @@
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Uw gegevens worden opgeslagen…",
|
||||
"body": "Dit proces kan enkele minuten duren. Bedankt voor uw geduld."
|
||||
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
|
||||
},
|
||||
"beta": {
|
||||
"heading": "Word betatester",
|
||||
@@ -255,9 +259,14 @@
|
||||
"loading": "Opname gestarten",
|
||||
"linkMore": "Meer informatie"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Toegang beperkt",
|
||||
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een opname starten (beta).",
|
||||
"linkMore": "Meer informatie"
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Uw gegevens worden opgeslagen…",
|
||||
"body": "Dit proces kan enkele minuten duren. Bedankt voor uw geduld."
|
||||
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Opname bezig …",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { FeedbackRoute, RoomRoute, roomIdPattern } from '@/features/rooms'
|
||||
import {
|
||||
FeedbackRoute,
|
||||
RoomRoute,
|
||||
flexibleRoomIdPattern,
|
||||
} from '@/features/rooms'
|
||||
import { HomeRoute } from '@/features/home'
|
||||
import { LegalTermsRoute } from '@/features/legalsTerms/LegalTermsRoute'
|
||||
import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility'
|
||||
@@ -32,8 +36,8 @@ export const routes: Record<
|
||||
},
|
||||
room: {
|
||||
name: 'room',
|
||||
path: new RegExp(`^[/](?<roomId>${roomIdPattern})$`),
|
||||
to: (roomId: string) => `/${roomId.trim()}`,
|
||||
path: new RegExp(`^[/](?<roomId>${flexibleRoomIdPattern})$`),
|
||||
Component: RoomRoute,
|
||||
},
|
||||
feedback: {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
areFunnyEffectsEnabled: boolean
|
||||
}
|
||||
|
||||
export const konamiStore = proxy<State>({
|
||||
areFunnyEffectsEnabled: false,
|
||||
})
|
||||
@@ -67,6 +67,7 @@ backend:
|
||||
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
|
||||
SUMMARY_SERVICE_API_TOKEN: password
|
||||
SCREEN_RECORDING_BASE_URL: https://meet.127.0.0.1.nip.io/recordings
|
||||
ROOM_TELEPHONY_ENABLED: True
|
||||
SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
|
||||
|
||||
|
||||
@@ -75,6 +76,13 @@ backend:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
while ! python manage.py check --database default > /dev/null 2>&1
|
||||
do
|
||||
echo "Database not ready"
|
||||
sleep 2
|
||||
done
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py migrate --no-input &&
|
||||
python manage.py create_demo --force
|
||||
restartPolicy: Never
|
||||
@@ -91,6 +99,13 @@ backend:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
while ! python manage.py check --database default > /dev/null 2>&1
|
||||
do
|
||||
echo "Database not ready"
|
||||
sleep 2
|
||||
done
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
@@ -148,7 +163,7 @@ summary:
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
OPENAI_API_KEY: password
|
||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
||||
OPENAI_ASR_MODEL: large-v2
|
||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
||||
AWS_S3_SECURE_ACCESS: False
|
||||
WEBHOOK_API_TOKEN: password
|
||||
@@ -181,7 +196,7 @@ celery:
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
OPENAI_API_KEY: password
|
||||
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
|
||||
OPENAI_ASR_MODEL: openai/whisper-large-v3
|
||||
OPENAI_ASR_MODEL: large-v2
|
||||
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
|
||||
AWS_S3_SECURE_ACCESS: False
|
||||
WEBHOOK_API_TOKEN: password
|
||||
|
||||
@@ -94,6 +94,7 @@ backend:
|
||||
name: backend
|
||||
key: BREVO_API_KEY
|
||||
BREVO_API_CONTACT_LIST_IDS: 8
|
||||
ROOM_TELEPHONY_ENABLED: True
|
||||
SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
|
||||
|
||||
|
||||
@@ -102,6 +103,13 @@ backend:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
while ! python manage.py check --database default > /dev/null 2>&1
|
||||
do
|
||||
echo "Database not ready"
|
||||
sleep 2
|
||||
done
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py migrate --no-input &&
|
||||
python manage.py create_demo --force
|
||||
restartPolicy: Never
|
||||
@@ -118,6 +126,13 @@ backend:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
while ! python manage.py check --database default > /dev/null 2>&1
|
||||
do
|
||||
echo "Database not ready"
|
||||
sleep 2
|
||||
done
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: meet
|
||||
version: 0.0.5
|
||||
version: 0.0.9
|
||||
|
||||
@@ -4,101 +4,104 @@
|
||||
|
||||
### General configuration
|
||||
|
||||
| Name | Description | Value |
|
||||
| ---------------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `image.repository` | Repository to use to pull meet's container image | `lasuite/meet-backend` |
|
||||
| `image.tag` | meet's container tag | `latest` |
|
||||
| `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
|
||||
| `image.credentials.username` | Username for container registry authentication | |
|
||||
| `image.credentials.password` | Password for container registry authentication | |
|
||||
| `image.credentials.registry` | Registry url for which the credentials are specified | |
|
||||
| `image.credentials.name` | Name of the generated secret for imagePullSecrets | |
|
||||
| `nameOverride` | Override the chart name | `""` |
|
||||
| `fullnameOverride` | Override the full application name | `""` |
|
||||
| `ingress.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingress.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingress.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingress.path` | Path to use for the Ingress | `/` |
|
||||
| `ingress.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingress.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingress.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingress.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingress.customBackends` | Add custom backends to ingress | `[]` |
|
||||
| `ingressAdmin.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressAdmin.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressAdmin.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingressAdmin.path` | Path to use for the Ingress | `/admin` |
|
||||
| `ingressAdmin.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingressAdmin.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressMedia.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressMedia.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressMedia.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingressMedia.path` | Path to use for the Ingress | `/media/(.*)` |
|
||||
| `ingressMedia.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingressMedia.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingressMedia.tls.secretName` | Secret name for TLS config | `nil` |
|
||||
| `ingressMedia.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressMedia.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url` | | `https://meet.example.com/api/v1.0/recordings/media-auth/` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers` | | `Authorization, X-Amz-Date, X-Amz-Content-SHA256` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost` | | `minio.meet.svc.cluster.local:9000` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/configuration-snippet` | | `add_header Content-Security-Policy "default-src 'none'" always;` |
|
||||
| `serviceMedia.host` | | `minio.meet.svc.cluster.local` |
|
||||
| `serviceMedia.port` | | `9000` |
|
||||
| `serviceMedia.annotations` | | `{}` |
|
||||
| Name | Description | Value |
|
||||
| ---------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `image.repository` | Repository to use to pull meet's container image | `lasuite/meet-backend` |
|
||||
| `image.tag` | meet's container tag | `latest` |
|
||||
| `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
|
||||
| `image.credentials.username` | Username for container registry authentication | |
|
||||
| `image.credentials.password` | Password for container registry authentication | |
|
||||
| `image.credentials.registry` | Registry url for which the credentials are specified | |
|
||||
| `image.credentials.name` | Name of the generated secret for imagePullSecrets | |
|
||||
| `nameOverride` | Override the chart name | `""` |
|
||||
| `fullnameOverride` | Override the full application name | `""` |
|
||||
| `ingress.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingress.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingress.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingress.path` | Path to use for the Ingress | `/` |
|
||||
| `ingress.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingress.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingress.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingress.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingress.customBackends` | Add custom backends to ingress | `[]` |
|
||||
| `ingressAdmin.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressAdmin.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressAdmin.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingressAdmin.path` | Path to use for the Ingress | `/admin` |
|
||||
| `ingressAdmin.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingressAdmin.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressMedia.enabled` | whether to enable the Ingress or not | `false` |
|
||||
| `ingressMedia.className` | IngressClass to use for the Ingress | `nil` |
|
||||
| `ingressMedia.host` | Host for the Ingress | `meet.example.com` |
|
||||
| `ingressMedia.path` | Path to use for the Ingress | `/media/(.*)` |
|
||||
| `ingressMedia.hosts` | Additional host to configure for the Ingress | `[]` |
|
||||
| `ingressMedia.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
|
||||
| `ingressMedia.tls.secretName` | Secret name for TLS config | `nil` |
|
||||
| `ingressMedia.tls.additional[].secretName` | Secret name for additional TLS config | |
|
||||
| `ingressMedia.tls.additional[].hosts[]` | Hosts for additional TLS config | |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url` | | `https://meet.example.com/api/v1.0/recordings/media-auth/` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers` | | `Authorization, X-Amz-Date, X-Amz-Content-SHA256` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost` | | `minio.meet.svc.cluster.local:9000` |
|
||||
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/configuration-snippet` | | `add_header Content-Security-Policy "default-src 'none'" always;
|
||||
` |
|
||||
| `serviceMedia.host` | | `minio.meet.svc.cluster.local` |
|
||||
| `serviceMedia.port` | | `9000` |
|
||||
| `serviceMedia.annotations` | | `{}` |
|
||||
|
||||
### backend
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `backend.dpAnnotations` | Annotations to add to the backend Deployment | `{}` |
|
||||
| `backend.command` | Override the backend container command | `[]` |
|
||||
| `backend.args` | Override the backend container args | `[]` |
|
||||
| `backend.replicas` | Amount of backend replicas | `3` |
|
||||
| `backend.shareProcessNamespace` | Enable share process namespace between containers | `false` |
|
||||
| `backend.sidecars` | Add sidecars containers to backend deployment | `[]` |
|
||||
| `backend.migrateJobAnnotations` | Annotations for the migrate job | `{}` |
|
||||
| `backend.securityContext` | Configure backend Pod security context | `nil` |
|
||||
| `backend.envVars` | Configure backend container environment variables | `undefined` |
|
||||
| `backend.envVars.BY_VALUE` | Example environment variable by setting value directly | |
|
||||
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.name` | Name of a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `backend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
|
||||
| `backend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
|
||||
| `backend.podAnnotations` | Annotations to add to the backend Pod | `{}` |
|
||||
| `backend.service.type` | backend Service type | `ClusterIP` |
|
||||
| `backend.service.port` | backend Service listening port | `80` |
|
||||
| `backend.service.targetPort` | backend container listening port | `8000` |
|
||||
| `backend.service.annotations` | Annotations to add to the backend Service | `{}` |
|
||||
| `backend.migrate.command` | backend migrate command | `["python","manage.py","migrate","--no-input"]` |
|
||||
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
|
||||
| `backend.createsuperuser.command` | backend migrate command | `["/bin/sh","-c","python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD\n"]` |
|
||||
| `backend.createsuperuser.restartPolicy` | backend migrate job restart policy | `Never` |
|
||||
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
|
||||
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `30` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `30` |
|
||||
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
|
||||
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
|
||||
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `30` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `30` |
|
||||
| `backend.resources` | Resource requirements for the backend container | `{}` |
|
||||
| `backend.nodeSelector` | Node selector for the backend Pod | `{}` |
|
||||
| `backend.tolerations` | Tolerations for the backend Pod | `[]` |
|
||||
| `backend.affinity` | Affinity for the backend Pod | `{}` |
|
||||
| `backend.persistence` | Additional volumes to create and mount on the backend. Used for debugging purposes | `{}` |
|
||||
| `backend.persistence.volume-name.size` | Size of the additional volume | |
|
||||
| `backend.persistence.volume-name.type` | Type of the additional volume, persistentVolumeClaim or emptyDir | |
|
||||
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
|
||||
| Name | Description | Value |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `backend.dpAnnotations` | Annotations to add to the backend Deployment | `{}` |
|
||||
| `backend.command` | Override the backend container command | `[]` |
|
||||
| `backend.args` | Override the backend container args | `[]` |
|
||||
| `backend.replicas` | Amount of backend replicas | `3` |
|
||||
| `backend.shareProcessNamespace` | Enable share process namespace between containers | `false` |
|
||||
| `backend.sidecars` | Add sidecars containers to backend deployment | `[]` |
|
||||
| `backend.migrateJobAnnotations` | Annotations for the migrate job | `{}` |
|
||||
| `backend.jobs.ttlSecondsAfterFinished` | Period to wait before remove jobs | `30` |
|
||||
| `backend.jobs.backoffLimit` | Numbers of jobs retries | `2` |
|
||||
| `backend.securityContext` | Configure backend Pod security context | `nil` |
|
||||
| `backend.envVars` | Configure backend container environment variables | `undefined` |
|
||||
| `backend.envVars.BY_VALUE` | Example environment variable by setting value directly | |
|
||||
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.name` | Name of a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
|
||||
| `backend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
|
||||
| `backend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
|
||||
| `backend.podAnnotations` | Annotations to add to the backend Pod | `{}` |
|
||||
| `backend.service.type` | backend Service type | `ClusterIP` |
|
||||
| `backend.service.port` | backend Service listening port | `80` |
|
||||
| `backend.service.targetPort` | backend container listening port | `8000` |
|
||||
| `backend.service.annotations` | Annotations to add to the backend Service | `{}` |
|
||||
| `backend.migrate.command` | backend migrate command | `["/bin/sh","-c","while ! python manage.py check --database default > /dev/null 2>&1\ndo\n echo \"Database not ready\"\n sleep 2\ndone\necho \"Database is ready\"\n\npython manage.py migrate --no-input\n"]` |
|
||||
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
|
||||
| `backend.createsuperuser.command` | backend migrate command | `["/bin/sh","-c","while ! python manage.py check --database default > /dev/null 2>&1\ndo\n echo \"Database not ready\"\n sleep 2\ndone\necho \"Database is ready\"\n\npython manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD\n"]` |
|
||||
| `backend.createsuperuser.restartPolicy` | backend migrate job restart policy | `Never` |
|
||||
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
|
||||
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `30` |
|
||||
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `30` |
|
||||
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
|
||||
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
|
||||
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
|
||||
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `30` |
|
||||
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `30` |
|
||||
| `backend.resources` | Resource requirements for the backend container | `{}` |
|
||||
| `backend.nodeSelector` | Node selector for the backend Pod | `{}` |
|
||||
| `backend.tolerations` | Tolerations for the backend Pod | `[]` |
|
||||
| `backend.affinity` | Affinity for the backend Pod | `{}` |
|
||||
| `backend.persistence` | Additional volumes to create and mount on the backend. Used for debugging purposes | `{}` |
|
||||
| `backend.persistence.volume-name.size` | Size of the additional volume | |
|
||||
| `backend.persistence.volume-name.type` | Type of the additional volume, persistentVolumeClaim or emptyDir | |
|
||||
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
|
||||
|
||||
### frontend
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ metadata:
|
||||
labels:
|
||||
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
ttlSecondsAfterFinished: {{ .Values.backend.jobs.ttlSecondsAfterFinished }}
|
||||
backoffLimit: {{ .Values.backend.jobs.backoffLimit }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
@@ -75,10 +77,6 @@ spec:
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -14,6 +14,8 @@ metadata:
|
||||
labels:
|
||||
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
|
||||
spec:
|
||||
ttlSecondsAfterFinished: {{ .Values.backend.jobs.ttlSecondsAfterFinished }}
|
||||
backoffLimit: {{ .Values.backend.jobs.backoffLimit }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
@@ -75,10 +77,6 @@ spec:
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -60,7 +60,7 @@ spec:
|
||||
serviceName: {{ include "meet.frontend.fullname" . }}
|
||||
servicePort: {{ .Values.frontend.service.port }}
|
||||
{{- end }}
|
||||
- path: /api
|
||||
- path: /api/
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
@@ -96,7 +96,7 @@ spec:
|
||||
serviceName: {{ include "meet.frontend.fullname" $ }}
|
||||
servicePort: {{ $.Values.frontend.service.port }}
|
||||
{{- end }}
|
||||
- path: /api
|
||||
- path: /api/
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
|
||||
@@ -136,6 +136,12 @@ backend:
|
||||
## @param backend.migrateJobAnnotations Annotations for the migrate job
|
||||
migrateJobAnnotations: {}
|
||||
|
||||
## @param backend.jobs.ttlSecondsAfterFinished Period to wait before remove jobs
|
||||
## @param backend.jobs.backoffLimit Numbers of jobs retries
|
||||
jobs:
|
||||
ttlSecondsAfterFinished: 30
|
||||
backoffLimit: 2
|
||||
|
||||
## @param backend.securityContext Configure backend Pod security context
|
||||
securityContext: null
|
||||
|
||||
@@ -166,10 +172,17 @@ backend:
|
||||
## @param backend.migrate.restartPolicy backend migrate job restart policy
|
||||
migrate:
|
||||
command:
|
||||
- "python"
|
||||
- "manage.py"
|
||||
- "migrate"
|
||||
- "--no-input"
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
while ! python manage.py check --database default > /dev/null 2>&1
|
||||
do
|
||||
echo "Database not ready"
|
||||
sleep 2
|
||||
done
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py migrate --no-input
|
||||
restartPolicy: Never
|
||||
|
||||
## @param backend.createsuperuser.command backend migrate command
|
||||
@@ -179,6 +192,13 @@ backend:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- |
|
||||
while ! python manage.py check --database default > /dev/null 2>&1
|
||||
do
|
||||
echo "Database not ready"
|
||||
sleep 2
|
||||
done
|
||||
echo "Database is ready"
|
||||
|
||||
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
|
||||
restartPolicy: Never
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.14.0"
|
||||
"node": "22.15.0"
|
||||
},
|
||||
"repository": "https://github.com/numerique-gouv/meet",
|
||||
"author": "DINUM",
|
||||
|
||||
@@ -853,16 +853,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
|
||||
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
|
||||
@@ -1616,26 +1606,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
|
||||
"integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.25.9",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.25.9",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"react-refresh": "^0.14.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
@@ -2023,29 +1993,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
|
||||
"integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-refresh": {
|
||||
"version": "0.4.18",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz",
|
||||
"integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8.40"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
|
||||
@@ -2303,19 +2250,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.14.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
|
||||
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
@@ -3031,43 +2965,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.22.0.tgz",
|
||||
"integrity": "sha512-Y2rj210FW1Wb6TWXzQc5+P+EWI9/zdS57hLEc0gnyuvdzWo8+Y8brKlbj0muejonhMI/xAZCnZZwjbIfv1CkOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.22.0",
|
||||
"@typescript-eslint/parser": "8.22.0",
|
||||
"@typescript-eslint/utils": "8.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
|
||||
@@ -3109,78 +3006,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
|
||||
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"postcss": "^8.4.49",
|
||||
"rollup": "^4.23.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -14,16 +14,16 @@
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.17.0",
|
||||
"@eslint/js": "9.26.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"eslint-plugin-react-refresh": "0.4.18",
|
||||
"globals": "15.14.0",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.18.2",
|
||||
"vite": "6.2.5"
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"globals": "16.1.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.32.0",
|
||||
"vite": "6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.22",
|
||||
"type": "module",
|
||||
"main": "./dist/visio-sdk.umd.cjs",
|
||||
"module": "./dist/visio-sdk.js",
|
||||
@@ -31,22 +31,22 @@
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "5.64.2",
|
||||
"@types/node": "20.14.2",
|
||||
"@tanstack/eslint-plugin-query": "5.74.7",
|
||||
"@types/node": "22.15.17",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@vitejs/plugin-react-swc": "3.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.0",
|
||||
"@typescript-eslint/parser": "8.32.0",
|
||||
"@vitejs/plugin-react-swc": "3.9.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.7",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"sass": "1.83.4",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.4.18",
|
||||
"vite-plugin-dts": "3.9.1"
|
||||
"sass": "1.88.0",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "5.4.19",
|
||||
"vite-plugin-dts": "4.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "0.1.21"
|
||||
version = "0.1.23"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
@@ -36,6 +36,28 @@ if settings.sentry_dsn and settings.sentry_is_enabled:
|
||||
sentry_sdk.init(dsn=settings.sentry_dsn, enable_tracing=True)
|
||||
|
||||
|
||||
DEFAULT_EMPTY_TRANSCRIPTION = """
|
||||
**Aucun contenu audio n’a été détecté dans votre transcription.**
|
||||
|
||||
|
||||
*Si vous pensez qu’il s’agit d’une erreur, n’hésitez pas à contacter
|
||||
notre support technique : visio@numerique.gouv.fr*
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
.
|
||||
|
||||
Quelques points que nous vous conseillons de vérifier :
|
||||
- Un micro était-il activé ?
|
||||
- Étiez-vous suffisamment proche ?
|
||||
- Le micro est-il de bonne qualité ?
|
||||
- L’enregistrement dure-t-il plus de 30 secondes ?
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def save_audio_stream(audio_stream, chunk_size=32 * 1024):
|
||||
"""Save an audio stream to a temporary OGG file."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
|
||||
@@ -64,9 +86,9 @@ def format_segments(transcription_data):
|
||||
conversation with speaker labels.
|
||||
"""
|
||||
formatted_output = ""
|
||||
if not transcription_data or not hasattr(transcription_data, 'segments'):
|
||||
if isinstance(transcription_data, dict) and 'segments' in transcription_data:
|
||||
segments = transcription_data['segments']
|
||||
if not transcription_data or not hasattr(transcription_data, "segments"):
|
||||
if isinstance(transcription_data, dict) and "segments" in transcription_data:
|
||||
segments = transcription_data["segments"]
|
||||
else:
|
||||
return "Error: Invalid transcription data format"
|
||||
else:
|
||||
@@ -75,8 +97,8 @@ def format_segments(transcription_data):
|
||||
previous_speaker = None
|
||||
|
||||
for segment in segments:
|
||||
speaker = segment.get('speaker', 'UNKNOWN_SPEAKER')
|
||||
text = segment.get('text', '')
|
||||
speaker = segment.get("speaker", "UNKNOWN_SPEAKER")
|
||||
text = segment.get("text", "")
|
||||
if text:
|
||||
if speaker != previous_speaker:
|
||||
formatted_output += f"\n\n **{speaker}**: {text}"
|
||||
@@ -85,6 +107,7 @@ def format_segments(transcription_data):
|
||||
previous_speaker = speaker
|
||||
return formatted_output
|
||||
|
||||
|
||||
def post_with_retries(url, data):
|
||||
"""Send POST request with automatic retries."""
|
||||
session = create_retry_session()
|
||||
@@ -218,7 +241,11 @@ def process_audio_transcribe_summarize_v2(filename: str, email: str, sub: str):
|
||||
os.remove(temp_file_path)
|
||||
logger.debug("Temporary file removed: %s", temp_file_path)
|
||||
|
||||
formatted_transcription = format_segments(transcription)
|
||||
formatted_transcription = (
|
||||
DEFAULT_EMPTY_TRANSCRIPTION
|
||||
if not transcription.segments
|
||||
else format_segments(transcription)
|
||||
)
|
||||
|
||||
data = {
|
||||
"title": "Transcription",
|
||||
@@ -236,5 +263,3 @@ def process_audio_transcribe_summarize_v2(filename: str, email: str, sub: str):
|
||||
logger.debug("Response body: %s", response.text)
|
||||
|
||||
# TODO - integrate summarize the transcript and create a new document.
|
||||
|
||||
|
||||
|
||||