Compare commits

..

2 Commits

Author SHA1 Message Date
Florent Chehab c018feb873 (backend) add file upload feature & tests
For the coming features we will need to store files on the meet side.
(for instance user backgrounds).

This commits adds a new Model to manage files, and the associated
serializers & viewsets. All are tested.

This work was heavily inspired by the work done by our friends at
https://github.com/suitenumerique/drive
It build on the same architecture design (upload directly to S3 but
download goes through our proxy), but model is much much simplier
(no folders, no file sharing, etc.).
2026-03-06 11:21:56 +01:00
Florent Chehab 6eaa782095 (backend) add mimetype detection logic from drive
Reused the logic developed by the team working on drive.
This is usefull for our own upload file backend (that will
come in later commits).

Dockefile was updated to add a required system dependency.

We might want to put this shared logic in a lib.
2026-03-06 11:21:56 +01:00
35 changed files with 99 additions and 343 deletions
+5 -7
View File
@@ -24,15 +24,13 @@ data
.cache
.circleci
.git
.vscode
.iml
.idea
db.sqlite3
.mypy_cache
.pylint.d
**/.idea
**/.vscode
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
.pytest_cache
# Frontend
**/node_modules
node_modules
+7 -13
View File
@@ -8,16 +8,15 @@ and this project adheres to
## [Unreleased]
### Changed
- ♿️(frontend) Caption text size setting for accessibility #1062
- ♿️(frontend) sync html lang attribute with i18n for screen readers #1111
- ♿️(frontend) improve MoreLink a11y and UX on home page #1112
- ♿(frontend) improve chat toast a11y for screen readers #1109
- ♿(frontend) improve ui and qria labels for help article links #1108
## [1.10.0] - 2026-03-05
### Fixed
- 🩹(frontend) remove incorrect reference to ProConnect on the prejoin #1080
- ✨(frontend) add Ctrl+Shift+/ to open shortcuts settings #1050
- ♿(frontend) announce selected state to screen readers #1081
- 💄(frontend) truncate long names with ellipsis in reaction overlay #1099
### Changed
- 🔒️(backend) enhance API input validation to strengthen security #1053
@@ -30,11 +29,6 @@ and this project adheres to
- 🐛(migrations) use settings in migrations #1058
- 💄(frontend) truncate pinned participant name with ellipsis on overflow #1056
- ♿(frontend) prevent focus ring clipping on invite dialog #1078
- ♿(frontend) dynamic tab title when connected to meeting #1060
- 🩹(frontend) remove incorrect reference to ProConnect on the prejoin #1080
- ✨(frontend) add Ctrl+Shift+/ to open shortcuts settings #1050
- ♿(frontend) announce selected state to screen readers #1081
- 💄(frontend) truncate long names with ellipsis in reaction overlay #1099
### Added
+7 -7
View File
@@ -122,11 +122,11 @@ 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. NOTE: Before you proceed, and is using the kind method, make sure you download this repo examples/helm directory and its contents to the location where you will be executing the helm command. Helm will look for "examples/helm/<name>values.yaml" from based on the path it is being executed.
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
$ helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak -f examples/helm/keycloak.values.yaml
$ helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak -f examples/keycloak.values.yaml
$ #wait until
$ kubectl get po
NAME READY STATUS RESTARTS AGE
@@ -150,7 +150,7 @@ OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
```
You can find these values in **examples/helm/keycloak.values.yaml**
You can find these values in **examples/keycloak.values.yaml**
### Find livekit server connexion values
@@ -159,7 +159,7 @@ LaSuite Meet use livekit for streaming part so if you have a livekit provider, o
Livekit need a redis (and meet too) so we will start by deploying a redis :
```
$ helm install redis oci://registry-1.docker.io/bitnamicharts/redis -f examples/helm/redis.values.yaml
$ helm install redis oci://registry-1.docker.io/bitnamicharts/redis -f examples/redis.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 26m
@@ -172,7 +172,7 @@ When the redis is ready we can deploy livekit-server.
```
$ helm repo add livekit https://helm.livekit.io
$ helm repo update
$ helm install livekit livekit/livekit-server -f examples/helm/livekit.values.yaml
$ helm install livekit livekit/livekit-server -f examples/livekit.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 30m
@@ -199,7 +199,7 @@ CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
LaSuite Meet uses a postgresql db as backend so if you have a provider, obtain the necessary information to use it. If you do not have, you can install a postgresql testing environment as follows:
```
$ helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql -f examples/helm/postgresql.values.yaml
$ helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql -f examples/postgresql.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 45m
@@ -226,7 +226,7 @@ Now you are ready to deploy LaSuite Meet without AI. AI required more dependenci
```
$ helm repo add meet https://suitenumerique.github.io/meet/
$ helm repo update
$ helm install meet meet/meet -f examples/helm/meet.values.yaml
$ helm install meet meet/meet -f examples/meet.values.yaml
```
## Test your deployment
+2 -2
View File
@@ -4,7 +4,7 @@ import { Suspense } from 'react'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClientProvider } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useTitle } from 'hoofd'
import { useLang } from 'hoofd'
import { Switch, Route } from 'wouter'
import { I18nProvider } from 'react-aria-components'
import { Layout } from './layout/Layout'
@@ -17,7 +17,7 @@ import { useIsSdkContext } from '@/features/sdk/hooks/useIsSdkContext'
function App() {
const { i18n } = useTranslation()
useTitle(import.meta.env.VITE_APP_TITLE ?? '')
useLang(i18n.language)
const isSDKContext = useIsSdkContext()
@@ -2,25 +2,23 @@ import { A, Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useConfig } from '@/api/useConfig'
const appTitle = import.meta.env.VITE_APP_TITLE ?? 'LaSuite Meet'
export const MoreLink = () => {
const { t } = useTranslation('home')
const { data } = useConfig()
if (!data?.manifest_link) return null
if (!data?.manifest_link) return
return (
<Text as="p" variant="sm" style={{ padding: '1rem 0' }}>
<Text as={'p'} variant={'sm'} style={{ padding: '1rem 0' }}>
<A
href={data?.manifest_link}
target="_blank"
rel="noopener noreferrer"
externalIcon
aria-label={t('moreLinkLabel', { appTitle })}
aria-label={t('moreLinkLabel')}
>
{t('moreLink')} {t('moreAbout', { appTitle })}
</A>
{t('moreLink')}
</A>{' '}
{t('moreAbout', { appTitle: `${import.meta.env.VITE_APP_TITLE}` })}
</Text>
)
}
+14 -1
View File
@@ -258,10 +258,23 @@ export const Home = () => {
</DialogTrigger>
</div>
<Separator />
<MoreLink />
<div
className={css({
display: { base: 'none', lg: 'inline' },
})}
>
<MoreLink />
</div>
</LeftColumn>
<RightColumn>
<IntroSlider />
<div
className={css({
display: { base: 'inline', lg: 'none' },
})}
>
<MoreLink />
</div>
</RightColumn>
</Columns>
<LaterMeetingDialog
@@ -18,15 +18,10 @@ import {
ANIMATION_DURATION,
ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal'
import { layoutStore } from '@/stores/layout'
import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
const { t } = useTranslation('notifications')
const announce = useScreenReaderAnnounce()
const [reactions, setReactions] = useState<Reaction[]>([])
const instanceIdRef = useRef(0)
@@ -46,21 +41,12 @@ export const MainNotificationToast = () => {
},
{ timeout: NotificationDuration.MESSAGE }
)
if (layoutStore.activePanelId !== PanelId.CHAT) {
announce(
t('chatMessageReceived', {
name: participant.name || t('defaultName'),
message: chatMessage.message,
}),
'polite'
)
}
}
room.on(RoomEvent.ChatMessage, handleChatMessage)
return () => {
room.off(RoomEvent.ChatMessage, handleChatMessage)
}
}, [room, triggerNotificationSound, announce, t])
}, [room, triggerNotificationSound])
const handleEmoji = (emoji: string, participant: Participant) => {
if (!emoji || !Object.values(Emoji).includes(emoji as Emoji)) return
@@ -79,20 +79,11 @@ export const NoAccessView = ({
})}
>
{t(`${i18nKey}.body`)}
<br />
{helpArticle && (
<>
{' '}
<A
href={helpArticle}
target="_blank"
rel="noopener noreferrer"
externalIcon
color="note"
aria-label={t(`${i18nKey}.linkAriaLabel`)}
>
{t(`${i18nKey}.linkMore`)}
</A>
</>
<A href={helpArticle} target="_blank">
{t(`${i18nKey}.linkMore`)}
</A>
)}
</Text>
</VStack>
@@ -151,16 +151,12 @@ export const ScreenRecordingSidePanel = () => {
</H>
<Text variant="body" fullWidth>
{recordingMaxDuration
? t('body', { max_duration: recordingMaxDuration })
? t('body', {
max_duration: recordingMaxDuration,
})
: t('bodyWithoutMaxDuration')}{' '}
{data?.support?.help_article_recording && (
<A
href={data.support.help_article_recording}
target="_blank"
rel="noopener noreferrer"
externalIcon
aria-label={t('linkAriaLabel')}
>
<A href={data.support.help_article_recording} target="_blank">
{t('linkMore')}
</A>
)}
@@ -187,16 +187,12 @@ export const TranscriptSidePanel = () => {
</H>
<Text variant="body" fullWidth>
{recordingMaxDuration
? t('body', { max_duration: recordingMaxDuration })
? t('body', {
max_duration: recordingMaxDuration,
})
: t('bodyWithoutMaxDuration')}{' '}
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
rel="noopener noreferrer"
externalIcon
aria-label={t('linkAriaLabel')}
>
<A href={data.support.help_article_transcript} target="_blank">
{t('linkMore')}
</A>
)}
@@ -212,7 +208,6 @@ export const TranscriptSidePanel = () => {
href={data.transcription_destination}
target="_blank"
rel="noopener noreferrer"
externalIcon
>
{data.transcription_destination.replace('https://', '')}
</A>
@@ -148,22 +148,17 @@ export const Tools = () => {
className={css({
textStyle: 'sm',
paddingX: '0.75rem',
paddingTop: '0.25rem',
marginBottom: '1rem',
})}
>
{t('body')}{' '}
{data?.support?.help_article_more_tools && (
<A
href={data.support.help_article_more_tools}
target="_blank"
rel="noopener noreferrer"
externalIcon
color="note"
aria-label={t('linkAriaLabel')}
>
{t('moreLink')}
</A>
<>
<A href={data?.support?.help_article_more_tools} target="_blank">
{t('moreLink')}
</A>
.
</>
)}
</Text>
{isTranscriptEnabled && (
@@ -1,28 +0,0 @@
import { useTitle } from 'hoofd'
import { useRoomData } from './useRoomData'
import { useMemo } from 'react'
const APP_TITLE = import.meta.env.VITE_APP_TITLE ?? ''
/**
* Updates the browser tab title with the room name to help users easily find
* the meeting tab among many open tabs. Works on both the join screen and
* once connected.
*/
export const useRoomPageTitle = () => {
const roomData = useRoomData()
const pageTitle = useMemo(() => {
if (!roomData) {
return APP_TITLE
}
const roomLabel = roomData.name || roomData.slug || ''
if (!roomLabel) return APP_TITLE
return `${APP_TITLE} - ${roomLabel} `
}, [roomData])
useTitle(pageTitle)
}
@@ -151,12 +151,7 @@ export function Chat({ ...props }: ChatProps) {
minHeight={0}
overflowY="scroll"
>
<ul
className="lk-list lk-chat-messages"
ref={ulRef}
role="log"
aria-relevant="additions"
>
<ul className="lk-list lk-chat-messages" ref={ulRef}>
{renderedMessages}
</ul>
</Div>
@@ -30,7 +30,6 @@ import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingProvider } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { useConnectionObserver } from '../hooks/useConnectionObserver'
import { useRoomPageTitle } from '../hooks/useRoomPageTitle'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import { useSettingsDialog } from '@/features/settings'
@@ -114,7 +113,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
)
useConnectionObserver()
useRoomPageTitle()
useVideoResolutionSubscription()
useRegisterKeyboardShortcut({
@@ -4,7 +4,6 @@ import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import { accessibilityStore } from '@/stores/accessibility'
import { CaptionsSettings } from '@/features/subtitle/component/CaptionsSettings'
export type AccessibilityTabProps = Pick<TabPanelProps, 'id'>
@@ -33,7 +32,6 @@ export const AccessibilityTab = ({ id }: AccessibilityTabProps) => {
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
</li>
<CaptionsSettings />
</ul>
</TabPanel>
)
@@ -1,49 +0,0 @@
import { Field, H } from '@/primitives'
import { css } from '@/styled-system/css'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import {
accessibilityStore,
type CaptionTextSize,
CAPTION_TEXT_SIZE_OPTIONS,
} from '@/stores/accessibility'
export const CaptionsSettings = () => {
const { t } = useTranslation('settings', {
keyPrefix: 'accessibility.captions',
})
const snap = useSnapshot(accessibilityStore)
const captionTextSizeItems = useMemo(
() =>
CAPTION_TEXT_SIZE_OPTIONS.map((size) => ({
value: size,
label: t(`textSize.options.${size}`),
})),
[t]
)
return (
<li>
<H
lvl={3}
className={css({
marginBottom: '0.5rem',
})}
>
{t('heading')}
</H>
<Field
type="select"
label={t('textSize.label')}
items={captionTextSizeItems}
selectedKey={snap.captionTextSize}
onSelectionChange={(key) => {
accessibilityStore.captionTextSize = key as CaptionTextSize
}}
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
</li>
)
}
@@ -8,25 +8,6 @@ import { useRoomContext } from '@livekit/components-react'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { Participant, RoomEvent } from 'livekit-client'
import { useSnapshot } from 'valtio'
import {
accessibilityStore,
CAPTION_TEXT_SIZE_OPTIONS,
type CaptionTextSize,
} from '@/stores/accessibility'
const FONT_SIZE_CONFIG: Record<
CaptionTextSize,
{ fontSize: string; lineHeight: string }
> = {
small: { fontSize: '0.875rem', lineHeight: '1.2rem' },
medium: { fontSize: '1.5rem', lineHeight: '1.7rem' },
large: { fontSize: '2.25rem', lineHeight: '2.5rem' },
}
const CAPTION_FONT_SIZES = Object.fromEntries(
CAPTION_TEXT_SIZE_OPTIONS.map((size) => [size, FONT_SIZE_CONFIG[size]])
) as Record<CaptionTextSize, { fontSize: string; lineHeight: string }>
export interface TranscriptionSegment {
id: string
@@ -92,10 +73,8 @@ const useTranscriptionState = () => {
}
const Transcription = ({ row }: { row: TranscriptionRow }) => {
const { captionTextSize } = useSnapshot(accessibilityStore)
const participantColor = getParticipantColor(row.participant)
const participantName = getParticipantName(row.participant)
const { fontSize, lineHeight } = CAPTION_FONT_SIZES[captionTextSize]
const getDisplayText = (row: TranscriptionRow): string => {
return row.segments
@@ -137,9 +116,10 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => {
</Text>
<p
className={css({
fontSize: '1.5rem',
lineHeight: '1.7rem',
fontWeight: '400',
})}
style={{ fontSize, lineHeight }}
>
{displayText}
</p>
@@ -218,6 +198,7 @@ export const Subtitles = () => {
)
}
}
return rows
}, [transcriptionSegments])
+1 -9
View File
@@ -3,7 +3,6 @@ import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
const i18nDefaultNamespace = 'global'
const fallbackLng = 'fr'
i18n.setDefaultNamespace(i18nDefaultNamespace)
i18n
@@ -16,7 +15,7 @@ i18n
.use(LanguageDetector)
.init({
supportedLngs: ['en', 'fr', 'nl', 'de'],
fallbackLng,
fallbackLng: 'fr',
ns: i18nDefaultNamespace,
detection: {
order: ['localStorage', 'navigator'],
@@ -25,10 +24,3 @@ i18n
escapeValue: false,
},
})
.then(() => {
document.documentElement.setAttribute('lang', i18n.language || fallbackLng)
})
i18n.on('languageChanged', (lang) => {
document.documentElement.setAttribute('lang', lang)
})
+1 -1
View File
@@ -10,7 +10,7 @@
"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 über {{appTitle}} neues Tab",
"moreLinkLabel": "Mehr erfahren neues Tab",
"moreLink": "Mehr erfahren",
"moreAbout": "über {{appTitle}}",
"createMenu": {
@@ -9,7 +9,6 @@
},
"muted": "{{name}} hat dein Mikrofon stummgeschaltet. Kein Teilnehmer kann dich hören.",
"openChat": "Chat öffnen",
"chatMessageReceived": "{{name}} sagt: {{message}}",
"lowerHand": {
"auto": "Es scheint, dass du angefangen hast zu sprechen, daher wird deine Hand gesenkt.",
"dismiss": "Hand oben lassen"
+7 -14
View File
@@ -322,13 +322,11 @@
"closeButton": "{{content}} ausblenden"
},
"chat": {
"disclaimer": "Die Nachrichten sind nur für Teilnehmer zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Anrufs gelöscht.",
"messagesLog": "Chat-Nachrichten"
"disclaimer": "Die Nachrichten sind nur für Teilnehmer zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Anrufs gelöscht."
},
"moreTools": {
"body": "Greifen Sie auf weitere Tools zu, um Ihre Meetings zu verbessern.",
"linkAriaLabel": "Dokumentation zu den Tools öffnen - öffnet in neuem Fenster",
"moreLink": "Dokumentation öffnen",
"moreLink": "Mehr erfahren",
"tools": {
"transcript": {
"title": "Transkribieren",
@@ -372,13 +370,11 @@
"starting": "Wird gestartet…",
"anotherModeStarted": "Eine Aufnahme läuft. <br/> Wechseln Sie das Menü und stoppen Sie sie."
},
"linkMore": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zur Transkription öffnen - öffnet in neuem Fenster",
"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": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zum Transkriptionszugang öffnen - öffnet in neuem Fenster",
"linkMore": "Mehr erfahren",
"dividerLabel": "ODER",
"login": {
"heading": "Anmeldung erforderlich",
@@ -393,8 +389,7 @@
"premium": {
"heading": "Premium-Funktion",
"body": "Diese Funktion ist öffentlichen Bediensteten vorbehalten. Wenn Ihre E-Mail-Adresse nicht autorisiert ist, kontaktieren Sie bitte den Support, um Zugriff zu erhalten.",
"linkMore": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zum Premium-Zugang öffnen - öffnet in neuem Fenster",
"linkMore": "Mehr erfahren",
"dividerLabel": "ODER",
"login": {
"heading": "Anmeldung erforderlich",
@@ -411,8 +406,7 @@
"heading": "Diesen Anruf für später aufzeichnen",
"body": "Zeichnen Sie bis zu {{max_duration}} des Meetings auf.",
"bodyWithoutMaxDuration": "Zeichnen Sie Ihr Meeting unbegrenzt auf.",
"linkMore": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zur Aufzeichnung öffnen - öffnet in neuem Fenster",
"linkMore": "Mehr erfahren",
"details": {
"receiver": "Die Aufzeichnung wird an den Organisator und die Mitorganisatoren gesendet.",
"destination": "Diese Aufzeichnung wird vorübergehend auf unseren Servern gespeichert",
@@ -430,8 +424,7 @@
"notAdminOrOwner": {
"heading": "Zugriff eingeschränkt",
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Videoaufnahme (Beta) starten.",
"linkMore": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zum Aufzeichnungszugang öffnen - öffnet in neuem Fenster",
"linkMore": "Mehr erfahren",
"dividerLabel": "ODER",
"login": {
"heading": "Anmeldung erforderlich",
-11
View File
@@ -116,17 +116,6 @@
"accessibility": {
"announceReactions": {
"label": "Reaktionen vorlesen"
},
"captions": {
"heading": "Untertitel",
"textSize": {
"label": "Untertitel-Schriftgröße",
"options": {
"small": "Klein",
"medium": "Mittel",
"large": "Groß"
}
}
}
},
"tabs": {
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "You can join a meeting by pasting its full link in the browser's address bar.",
"joinMeetingTipHeading": "Did you know?",
"loginToCreateMeeting": "Login to create a meeting",
"moreLinkLabel": "Learn more about {{appTitle}} - new tab",
"moreLinkLabel": "Learn more - new tab",
"moreLink": "Learn more",
"moreAbout": "about {{appTitle}}",
"createMenu": {
@@ -9,7 +9,6 @@
},
"muted": "{{name}} has muted your microphone. No participant can hear you.",
"openChat": "Open chat",
"chatMessageReceived": "{{name}} says: {{message}}",
"lowerHand": {
"auto": "It seems you have started speaking, so your hand will be lowered.",
"dismiss": "Keep hand raised"
+7 -14
View File
@@ -322,13 +322,11 @@
"closeButton": "Hide {{content}}"
},
"chat": {
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call.",
"messagesLog": "Chat messages"
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
},
"moreTools": {
"body": "Access more tools to enhance your meetings.",
"linkAriaLabel": "Open documentation about tools - opens in new window",
"moreLink": "Open documentation",
"moreLink": "Learn more",
"tools": {
"transcript": {
"title": "Transcribe",
@@ -372,13 +370,11 @@
"starting": "Starting…",
"anotherModeStarted": "Screen recording is running. Switch panels and stop it."
},
"linkMore": "Open documentation",
"linkAriaLabel": "Open documentation about transcription - opens in new window",
"linkMore": "Learn more",
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a transcription (beta).",
"linkMore": "Open documentation",
"linkAriaLabel": "Open documentation about transcription access - opens in new window",
"linkMore": "Learn more",
"dividerLabel": "OR",
"login": {
"heading": "Login Required",
@@ -393,8 +389,7 @@
"premium": {
"heading": "Premium feature",
"body": "This feature is reserved for public agents. If your email address is not authorized, please contact support to get access.",
"linkMore": "Open documentation",
"linkAriaLabel": "Open documentation about premium access - opens in new window",
"linkMore": "Learn more",
"dividerLabel": "OR",
"login": {
"heading": "You are not logged in!",
@@ -411,8 +406,7 @@
"heading": "Record this call for later",
"body": "Record up to {{max_duration}} of meeting.",
"bodyWithoutMaxDuration": "Record your meeting without limit.",
"linkMore": "Open documentation",
"linkAriaLabel": "Open documentation about recording - opens in new window",
"linkMore": "Learn more",
"details": {
"receiver": "The recording will be sent to the host and co-hosts.",
"destination": "This recording will be temporarily stored on our servers",
@@ -430,8 +424,7 @@
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a recording (beta).",
"linkMore": "Open documentation",
"linkAriaLabel": "Open documentation about recording access - opens in new window",
"linkMore": "Learn more",
"dividerLabel": "OR",
"login": {
"heading": "Login Required",
-11
View File
@@ -116,17 +116,6 @@
"accessibility": {
"announceReactions": {
"label": "Announce reactions aloud"
},
"captions": {
"heading": "Captions",
"textSize": {
"label": "Caption text size",
"options": {
"small": "Small",
"medium": "Medium",
"large": "Large"
}
}
}
},
"tabs": {
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "Vous pouvez rejoindre une réunion en copiant directement son lien complet dans la barre d'adresse du navigateur.",
"joinMeetingTipHeading": "Astuce",
"loginToCreateMeeting": "Connectez-vous pour créer une réunion",
"moreLinkLabel": "En savoir plus sur {{appTitle}} - nouvelle fenêtre",
"moreLinkLabel": "En savoir plus - nouvelle fenêtre",
"moreLink": "En savoir plus",
"moreAbout": "sur {{appTitle}}",
"createMenu": {
@@ -9,7 +9,6 @@
},
"muted": "{{name}} a coupé votre micro. Aucun participant ne peut l'entendre.",
"openChat": "Ouvrir le chat",
"chatMessageReceived": "{{name}} dit : {{message}}",
"lowerHand": {
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
"dismiss": "Laisser la main levée"
+9 -16
View File
@@ -322,13 +322,11 @@
"closeButton": "Masquer {{content}}"
},
"chat": {
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel.",
"messagesLog": "Messages du chat"
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
},
"moreTools": {
"body": "Accéder à davantage d'outils pour améliorer vos réunions.",
"linkAriaLabel": "Ouvrir la documentation sur les outils - ouvre dans une nouvelle fenêtre",
"moreLink": "Ouvrir la documentation",
"body": "Accèder à d'avantage d'outils pour améliorer vos réunions.",
"moreLink": "En savoir plus",
"tools": {
"transcript": {
"title": "Transcrire",
@@ -372,13 +370,11 @@
"starting": "Démarrage…",
"anotherModeStarted": "Un enregistrement est en cours. <br/> Changez de menu et arrêtez le."
},
"linkMore": "Ouvrir la documentation",
"linkAriaLabel": "Ouvrir la documentation sur la transcription - ouvre dans une nouvelle fenêtre",
"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": "Ouvrir la documentation",
"linkAriaLabel": "Ouvrir la documentation sur l'accès à la transcription - ouvre dans une nouvelle fenêtre",
"linkMore": "En savoir plus",
"dividerLabel": "OU",
"login": {
"heading": "Connexion requise",
@@ -393,7 +389,7 @@
"premium": {
"heading": "Fonctionnalité premium",
"body": "Cette fonctionnalité est réservée aux agents publics. Si votre adresse email nest pas autorisée, contactez le support pour obtenir l'accès.",
"linkMore": "Ouvrir la documentation",
"linkMore": "En savoir plus",
"dividerLabel": "OU",
"login": {
"heading": "Vous n'êtes pas connecté !",
@@ -403,16 +399,14 @@
"heading": "Demander à l'organisateur",
"body": "L'hôte recevra une notification et pourra démarrer la transcription pour vous.",
"buttonLabel": "Demander"
},
"linkAriaLabel": "Ouvrir la documentation sur l'accès premium - ouvre dans une nouvelle fenêtre"
}
}
},
"screenRecording": {
"heading": "Enregistrez cet appel pour plus tard",
"body": "Enregistrez jusqu'à {{max_duration}} de réunion.",
"bodyWithoutMaxDuration": "Enregistrez votre réunion sans limite.",
"linkMore": "Ouvrir la documentation",
"linkAriaLabel": "Ouvrir la documentation sur l'enregistrement - ouvre dans une nouvelle fenêtre",
"linkMore": "En savoir plus",
"details": {
"receiver": "L'enregistrement sera envoyé à l'organisateur et aux coorganisateurs.",
"destination": "Cet enregistrement sera conservé temporairement sur nos serveurs",
@@ -430,8 +424,7 @@
"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": "Ouvrir la documentation",
"linkAriaLabel": "Ouvrir la documentation sur l'accès à l'enregistrement - ouvre dans une nouvelle fenêtre",
"linkMore": "En savoir plus",
"dividerLabel": "OU",
"login": {
"heading": "Connexion requise",
-11
View File
@@ -116,17 +116,6 @@
"accessibility": {
"announceReactions": {
"label": "Vocaliser les réactions"
},
"captions": {
"heading": "Sous-titres",
"textSize": {
"label": "Taille du texte des sous-titres",
"options": {
"small": "Petit",
"medium": "Moyen",
"large": "Grand"
}
}
}
},
"tabs": {
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "U kunt deelnemen aan een vergadering door de volledige link in de adresbalk van de browser te plakken.",
"joinMeetingTipHeading": "Wist u dat?",
"loginToCreateMeeting": "Log in om een vergadering te maken",
"moreLinkLabel": "Meer informatie over {{appTitle}} - nieuw tabblad",
"moreLinkLabel": "Meer informatie - nieuw tabblad",
"moreLink": "Meer informatie",
"moreAbout": "over {{appTitle}}",
"createMenu": {
@@ -9,7 +9,6 @@
},
"muted": "{{name}} heeft uw microfoon gedempt. Deelnemers kunnen u niet horen.",
"openChat": "Open chat",
"chatMessageReceived": "{{name}} zegt: {{message}}",
"lowerHand": {
"auto": "Het lijkt erop dat u bent begonnen te spreken, dus we laten uw hand zakken.",
"dismiss": "Houdt uw hand opgestoken"
+7 -14
View File
@@ -322,13 +322,11 @@
"closeButton": "Verberg {{content}}"
},
"chat": {
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek.",
"messagesLog": "Chatberichten"
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek."
},
"moreTools": {
"body": "Je krijgt toegang tot meer tools om je vergaderingen te verbeteren.",
"linkAriaLabel": "Documentatie over tools openen - opent in nieuw venster",
"moreLink": "Documentatie openen",
"moreLink": "Lees meer",
"tools": {
"transcript": {
"title": "Transcriberen",
@@ -372,13 +370,11 @@
"starting": "Wordt gestart…",
"anotherModeStarted": "Er loopt een opname. <br/> Ga naar het menu en stop deze."
},
"linkMore": "Documentatie openen",
"linkAriaLabel": "Documentatie over transcriptie openen - opent in nieuw venster",
"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": "Documentatie openen",
"linkAriaLabel": "Documentatie over transcriptietoegang openen - opent in nieuw venster",
"linkMore": "Meer informatie",
"dividerLabel": "OF",
"login": {
"heading": "Inloggen vereist",
@@ -393,8 +389,7 @@
"premium": {
"heading": "Premiumfunctie",
"body": "Deze functie is voorbehouden aan openbare medewerkers. Als uw e-mailadres niet is toegestaan, neem dan contact op met de support om toegang te krijgen.",
"linkMore": "Documentatie openen",
"linkAriaLabel": "Documentatie over premiumtoegang openen - opent in nieuw venster",
"linkMore": "Meer informatie",
"dividerLabel": "OF",
"login": {
"heading": "Inloggen vereist",
@@ -411,8 +406,7 @@
"heading": "Neem dit gesprek op voor later",
"body": "Neem tot {{max_duration}} van de vergadering op.",
"bodyWithoutMaxDuration": "Neem je vergadering onbeperkt op.",
"linkMore": "Documentatie openen",
"linkAriaLabel": "Documentatie over opname openen - opent in nieuw venster",
"linkMore": "Meer informatie",
"details": {
"receiver": "De opname wordt verzonden naar de organisator en co-organisatoren.",
"destination": "Deze opname wordt tijdelijk op onze servers bewaard",
@@ -430,8 +424,7 @@
"notAdminOrOwner": {
"heading": "Toegang beperkt",
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een opname starten (beta).",
"linkMore": "Documentatie openen",
"linkAriaLabel": "Documentatie over opnametoegang openen - opent in nieuw venster",
"linkMore": "Meer informatie",
"dividerLabel": "OF",
"login": {
"heading": "Inloggen vereist",
-17
View File
@@ -113,29 +113,12 @@
"label": "Taal"
},
"settingsButtonLabel": "Instellingen",
"accessibility": {
"announceReactions": {
"label": "Reacties hardop aankondigen"
},
"captions": {
"heading": "Ondertitels",
"textSize": {
"label": "Ondertiteltekstgrootte",
"options": {
"small": "Klein",
"medium": "Gemiddeld",
"large": "Groot"
}
}
}
},
"tabs": {
"account": "Profiel",
"audio": "Audio",
"video": "Video",
"general": "Algemeen",
"notifications": "Meldingen",
"accessibility": "Toegankelijkheid",
"transcription": "Transcriptie",
"shortcuts": "Sneltoetsen"
}
-15
View File
@@ -2,22 +2,12 @@ import { proxy, subscribe } from 'valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys'
import { deserializeToProxyMap } from '@/utils/valtio'
export type CaptionTextSize = 'small' | 'medium' | 'large'
export const CAPTION_TEXT_SIZE_OPTIONS: CaptionTextSize[] = [
'small',
'medium',
'large',
]
type AccessibilityState = {
announceReactions: boolean
captionTextSize: CaptionTextSize
}
const DEFAULT_STATE: AccessibilityState = {
announceReactions: false,
captionTextSize: 'medium',
}
function getAccessibilityState(): AccessibilityState {
@@ -25,10 +15,6 @@ function getAccessibilityState(): AccessibilityState {
const stored = localStorage.getItem(STORAGE_KEYS.ACCESSIBILITY)
if (stored) {
const parsed = JSON.parse(stored)
const validCaptionSizes = CAPTION_TEXT_SIZE_OPTIONS
const captionTextSize = validCaptionSizes.includes(parsed.captionTextSize)
? parsed.captionTextSize
: DEFAULT_STATE.captionTextSize
return {
...DEFAULT_STATE,
...parsed,
@@ -36,7 +22,6 @@ function getAccessibilityState(): AccessibilityState {
typeof parsed.announceReactions === 'boolean'
? parsed.announceReactions
: DEFAULT_STATE.announceReactions,
captionTextSize,
}
}