Compare commits

...

1 Commits

Author SHA1 Message Date
lebaudantoine 3bf777965d ✨(frontend) warn users when the connection falls back to TURN
Highlight in the connection test when the user is connecting
through a TURN relay, especially over TLS or TCP. This usually
indicates that some network configuration is required on their
side, and gives them a concrete signal to pass to their IT team.

Suggested by a technical user, this is a first step toward making
users more autonomous when troubleshooting access to the tool.

Follow-up: show a similar warning in-product when we detect a
mid-meeting fallback to TURN/TLS. A one-time hint for first-time
users would likely be enough.
2026-10-03 23:18:30 +02:00
16 changed files with 222 additions and 34 deletions
+5 -1
View File
@@ -16,6 +16,11 @@ and this project adheres to
- ✨(frontend) let signed-out visitors start a meeting - ✨(frontend) let signed-out visitors start a meeting
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration - ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
### Changed
- ✨(frontend) warn users when the connection falls back to TURN
- 🔧(backend) configure the technical documentation url
### Fixed ### Fixed
- 🐛(frontend) enforce recording-mode permissions on the checkboxes - 🐛(frontend) enforce recording-mode permissions on the checkboxes
@@ -156,7 +161,6 @@ and this project adheres to
### Added ### Added
- ✨(any) let any authenticated user manage the lobby on trusted rooms - ✨(any) let any authenticated user manage the lobby on trusted rooms
### Changed ### Changed
- 📱(frontend) collapse mobile control bar items on narrow viewports - 📱(frontend) collapse mobile control bar items on narrow viewports
+1
View File
@@ -347,6 +347,7 @@ These are the environmental options available on meet backend.
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true | | FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} | | FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | | | FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
| FRONTEND_TECHNICAL_DOCUMENTATION_URL | URL of the technical documentation (network prerequisites) linked from the footer and the connection test. If unset, both links are hidden | |
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false | | FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false | | FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | | DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
+5
View File
@@ -466,6 +466,11 @@ class Base(Configuration):
"documentation_url": values.Value( "documentation_url": values.Value(
None, environ_name="FRONTEND_DOCUMENTATION_URL", environ_prefix=None None, environ_name="FRONTEND_DOCUMENTATION_URL", environ_prefix=None
), ),
"technical_documentation_url": values.Value(
None,
environ_name="FRONTEND_TECHNICAL_DOCUMENTATION_URL",
environ_prefix=None,
),
"external_home_url": values.Value( "external_home_url": values.Value(
None, environ_name="FRONTEND_EXTERNAL_HOME_URL", environ_prefix=None None, environ_name="FRONTEND_EXTERNAL_HOME_URL", environ_prefix=None
), ),
+1
View File
@@ -22,6 +22,7 @@ export interface ApiConfig {
url: string url: string
} }
documentation_url?: string documentation_url?: string
technical_documentation_url?: string
external_home_url?: string external_home_url?: string
silence_livekit_debug_logs?: boolean silence_livekit_debug_logs?: boolean
is_silent_login_enabled?: boolean is_silent_login_enabled?: boolean
@@ -22,6 +22,12 @@ export type IceCandidateInfo = {
port?: number port?: number
/** Local candidates only, and not reported by every browser. */ /** Local candidates only, and not reported by every browser. */
networkType?: string networkType?: string
/**
* For a local relay candidate, the TURN URL it was gathered from
* (e.g. `turns:turn.example.com:443?transport=tcp`). Used as a fallback
* when the browser does not report `relayProtocol`.
*/
url?: string
} }
export type IceCandidatePair = { export type IceCandidatePair = {
@@ -42,6 +48,57 @@ export type IceCandidateReport = {
working: IceCandidatePair[] working: IceCandidatePair[]
} }
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null
/** Narrows the loosely typed `data` stored on a step result. */
export const isIceCandidateReport = (
data: unknown
): data is IceCandidateReport =>
isObject(data) &&
Array.isArray(data.working) &&
(data.selected === null ||
(isObject(data.selected) && isObject(data.selected.local)))
/**
* Transport between the browser and the TURN server for a local relay
* candidate: udp, tcp or tls, or undefined when it cannot be determined.
*
* `protocol` is deliberately not used here: on a relay candidate it describes
* the TURN allocation (server to peer), which is UDP even when the client
* reaches the TURN server over TLS.
*/
export const getRelayTransport = (
candidate: IceCandidateInfo
): string | undefined => {
if (candidate.relayProtocol) return candidate.relayProtocol.toLowerCase()
if (!candidate.url) return undefined
const url = candidate.url.toLowerCase()
if (url.startsWith('turns:')) return 'tls'
if (!url.startsWith('turn:')) return undefined
const transport = /[?&]transport=(udp|tcp)\b/.exec(url)?.[1]
// RFC 7065: a turn: URI without a transport parameter defaults to UDP.
return transport ?? 'udp'
}
/**
* True when the selected pair goes through a TURN relay reached over TCP or
* TLS. Media still flows, but TCP head-of-line blocking usually degrades
* audio and video under packet loss.
*
* Direct routes (host, srflx, prflx), including ICE-TCP to the SFU, are out of
* scope: the warning and its documentation are about TURN fallbacks.
* An undetermined transport is not evidence of a bad route.
*/
export const isRelayedOverTcp = (data: unknown): boolean => {
if (!isIceCandidateReport(data) || !data.selected) return false
const { local } = data.selected
if (local.type !== 'relay') return false
const transport = getRelayTransport(local)
return transport === 'tcp' || transport === 'tls'
}
const PROBE_WIDTH = 320 const PROBE_WIDTH = 320
const PROBE_HEIGHT = 180 const PROBE_HEIGHT = 180
const PROBE_FPS = 15 const PROBE_FPS = 15
@@ -57,6 +114,7 @@ const readCandidate = (stats?: Stats): IceCandidateInfo => {
protocol: stats.protocol as string | undefined, protocol: stats.protocol as string | undefined,
relayProtocol: stats.relayProtocol as string | undefined, relayProtocol: stats.relayProtocol as string | undefined,
networkType: stats.networkType as string | undefined, networkType: stats.networkType as string | undefined,
url: stats.url as string | undefined,
...(INCLUDE_CANDIDATE_ADDRESSES ...(INCLUDE_CANDIDATE_ADDRESSES
? { ? {
address: stats.address as string | undefined, address: stats.address as string | undefined,
@@ -67,7 +125,10 @@ const readCandidate = (stats?: Stats): IceCandidateInfo => {
} }
const describeCandidate = (candidate: IceCandidateInfo) => { const describeCandidate = (candidate: IceCandidateInfo) => {
const transport = candidate.relayProtocol ?? candidate.protocol ?? 'unknown' const transport =
(candidate.type === 'relay' ? getRelayTransport(candidate) : undefined) ??
candidate.protocol ??
'unknown'
const endpoint = const endpoint =
candidate.address === undefined candidate.address === undefined
? '' ? ''
@@ -2,18 +2,44 @@ import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ProgressBar } from 'react-aria-components' import { ProgressBar } from 'react-aria-components'
import { css, cx } from '@/styled-system/css' import { css, cx } from '@/styled-system/css'
import { A } from '@/primitives'
import { useConfig } from '@/api/useConfig'
import type { ConnectionTestStats } from '../types' import type { ConnectionTestStats } from '../types'
import { statusSquareClass } from './stepAppearance' import { statusSquareClass } from './stepAppearance'
type SummaryState = 'idle' | 'running' | 'passed' | 'partial' | 'failed' type SummaryState =
| 'idle'
| 'running'
| 'passed'
| 'partial'
| 'failed'
| 'warning'
/** Only a failure earns a colour: everything else stays near-black. */ /** Only a failure or a degraded route earns a colour: everything else stays near-black. */
const stateColorClass: Record<SummaryState, string> = { const stateColorClass: Record<SummaryState, string> = {
idle: css({ color: 'greyscale.1000' }), idle: css({ color: 'greyscale.1000' }),
running: css({ color: 'greyscale.1000' }), running: css({ color: 'greyscale.1000' }),
passed: css({ color: 'greyscale.1000' }), passed: css({ color: 'greyscale.1000' }),
partial: css({ color: 'greyscale.1000' }), partial: css({ color: 'greyscale.1000' }),
failed: css({ color: 'danger.600' }), failed: css({ color: 'danger.600' }),
warning: css({ color: 'warning' }),
}
/**
* A hard failure still outranks a warning step; a warning outranks 'partial'
* because a measured degraded route matters more than skipped camera or
* microphone checks.
*/
const getSummaryState = (
stats: ConnectionTestStats,
isRunning: boolean
): SummaryState => {
if (isRunning) return 'running'
if (!stats.hasStarted) return 'idle'
if (stats.failed > 0) return 'failed'
if (stats.warnings > 0) return 'warning'
if (stats.skipped > 0) return 'partial'
return 'passed'
} }
const cardClass = css({ const cardClass = css({
@@ -183,16 +209,15 @@ export const ConnectionTestSummary = ({
children?: ReactNode children?: ReactNode
}) => { }) => {
const { t } = useTranslation('connectionTest') const { t } = useTranslation('connectionTest')
const { data: config } = useConfig()
const state: SummaryState = isRunning // Network prerequisites for the reader's IT department. Instance specific,
? 'running' // so it comes from the backend; without it the warning shows no link.
: !stats.hasStarted const networkDocUrl = config?.technical_documentation_url
? 'idle'
: stats.failed > 0 const state = getSummaryState(stats, isRunning)
? 'failed' // Skipped device checks still deserve their hint under a route warning.
: stats.skipped > 0 const showPartialHint = state === 'warning' && stats.skipped > 0
? 'partial'
: 'passed'
return ( return (
<section className={cardClass}> <section className={cardClass}>
@@ -206,7 +231,27 @@ export const ConnectionTestSummary = ({
: t(`summary.${state}`)} : t(`summary.${state}`)}
</p> </p>
<p className={hintClass}>{t(`summary.${state}Hint`)}</p> <p className={hintClass}>
{t(`summary.${state}Hint`)}
{state === 'warning' && networkDocUrl && (
<>
{' '}
<A
href={networkDocUrl}
target="_blank"
rel="noopener noreferrer"
size="sm"
externalIcon
aria-label={t('summary.warningDocLinkAriaLabel')}
>
{t('summary.warningDocLink')}
</A>
</>
)}
</p>
{showPartialHint && (
<p className={hintClass}>{t('summary.partialHint')}</p>
)}
</div> </div>
{stats.hasStarted && ( {stats.hasStarted && (
@@ -237,6 +282,13 @@ export const ConnectionTestSummary = ({
value={stats.passed} value={stats.passed}
label={t('counts.passed')} label={t('counts.passed')}
/> />
{stats.warnings > 0 && (
<Counter
squareClass={statusSquareClass.warning}
value={stats.warnings}
label={t('counts.warnings')}
/>
)}
<Counter <Counter
squareClass={statusSquareClass.skipped} squareClass={statusSquareClass.skipped}
value={stats.skipped} value={stats.skipped}
@@ -15,15 +15,20 @@ export const statusSquareClass: Record<ConnectionTestStepStatus, string> = {
animation: 'pulse_background 1.2s ease-in-out infinite', animation: 'pulse_background 1.2s ease-in-out infinite',
}), }),
success: css({ backgroundColor: 'success.600' }), success: css({ backgroundColor: 'success.600' }),
warning: css({ backgroundColor: 'warning' }),
failed: css({ backgroundColor: 'danger.600' }), failed: css({ backgroundColor: 'danger.600' }),
skipped: css({ backgroundColor: 'greyscale.300' }), skipped: css({ backgroundColor: 'greyscale.300' }),
} }
/** Colour is carried by the square; the label stays near-black except on failure. */ /**
* Colour is carried by the square; the label stays near-black except on
* failure and warning.
*/
export const statusTextClass: Record<ConnectionTestStepStatus, string> = { export const statusTextClass: Record<ConnectionTestStepStatus, string> = {
pending: css({ color: 'greyscale.500' }), pending: css({ color: 'greyscale.500' }),
running: css({ color: 'greyscale.700' }), running: css({ color: 'greyscale.700' }),
success: css({ color: 'greyscale.1000' }), success: css({ color: 'greyscale.1000' }),
warning: css({ color: 'warning', fontWeight: 'medium' }),
failed: css({ color: 'danger.600', fontWeight: 'medium' }), failed: css({ color: 'danger.600', fontWeight: 'medium' }),
skipped: css({ color: 'greyscale.500' }), skipped: css({ color: 'greyscale.500' }),
} }
@@ -8,7 +8,10 @@ import {
type CheckInfo, type CheckInfo,
} from 'livekit-client' } from 'livekit-client'
import { fetchConnectionTestDetails } from '../api/fetchConnectionTestDetails' import { fetchConnectionTestDetails } from '../api/fetchConnectionTestDetails'
import { SelectedCandidateCheck } from '../checks/selectedCandidate' import {
isRelayedOverTcp,
SelectedCandidateCheck,
} from '../checks/selectedCandidate'
import { import {
createInitialSteps, createInitialSteps,
type ConnectionTestLog, type ConnectionTestLog,
@@ -49,10 +52,23 @@ const getErrorMessage = (error: unknown, fallback = 'Unknown error') =>
const isPermissionError = (error: unknown) => const isPermissionError = (error: unknown) =>
error instanceof Error && PERMISSION_ERROR_NAMES.has(error.name) error instanceof Error && PERMISSION_ERROR_NAMES.has(error.name)
const toStepStatus = (info: CheckInfo): ConnectionTestStepStatus => {
const status = CHECK_STATUS_TO_STEP[info.status] ?? 'failed'
return status === 'success' && isRelayedOverTcp(info.data)
? 'warning'
: status
}
const fromCheckInfo = (info: CheckInfo): Partial<ConnectionTestStepResult> => ({ const fromCheckInfo = (info: CheckInfo): Partial<ConnectionTestStepResult> => ({
status: CHECK_STATUS_TO_STEP[info.status] ?? 'failed', status: toStepStatus(info),
summary: info.description, summary: info.description,
logs: info.logs, logs: info.logs,
// Only SelectedCandidateCheck sets `data` (the ICE candidate report).
// Consumers narrow it with a type guard (see isIceCandidateReport).
data:
typeof info.data === 'object' && info.data !== null
? (info.data as Record<string, unknown>)
: undefined,
}) })
const groupDevicesByKind = (devices: MediaDeviceInfo[]) => { const groupDevicesByKind = (devices: MediaDeviceInfo[]) => {
@@ -15,6 +15,7 @@ export type ConnectionTestStepStatus =
| 'pending' | 'pending'
| 'running' | 'running'
| 'success' | 'success'
| 'warning'
| 'failed' | 'failed'
| 'skipped' | 'skipped'
@@ -63,6 +64,7 @@ export type ConnectionTestStats = {
total: number total: number
settled: number settled: number
passed: number passed: number
warnings: number
failed: number failed: number
skipped: number skipped: number
hasStarted: boolean hasStarted: boolean
@@ -77,24 +79,27 @@ export const summarizeSteps = (
steps: ConnectionTestStepResult[] steps: ConnectionTestStepResult[]
): ConnectionTestStats => { ): ConnectionTestStats => {
let passed = 0 let passed = 0
let warnings = 0
let failed = 0 let failed = 0
let skipped = 0 let skipped = 0
let pending = 0 let pending = 0
for (const step of steps) { for (const step of steps) {
if (step.status === 'success') passed += 1 if (step.status === 'success') passed += 1
else if (step.status === 'warning') warnings += 1
else if (step.status === 'failed') failed += 1 else if (step.status === 'failed') failed += 1
else if (step.status === 'skipped') skipped += 1 else if (step.status === 'skipped') skipped += 1
else if (step.status === 'pending') pending += 1 else if (step.status === 'pending') pending += 1
} }
const total = steps.length const total = steps.length
const settled = passed + failed + skipped const settled = passed + warnings + failed + skipped
return { return {
total, total,
settled, settled,
passed, passed,
warnings,
failed, failed,
skipped, skipped,
hasStarted: pending < total, hasStarted: pending < total,
+23 -16
View File
@@ -126,6 +126,9 @@ export const Footer = () => {
return null return null
} }
const isConnectionTestEnabled = !!data.diagnostics?.connection_test_enabled
const technicalDocumentationUrl = data.technical_documentation_url
return ( return (
<footer <footer
className={css({ className={css({
@@ -256,7 +259,9 @@ export const Footer = () => {
{t('links.data')} {t('links.data')}
</A> </A>
</StyledLi> </StyledLi>
<StyledLi divider> <StyledLi
divider={isConnectionTestEnabled || !!technicalDocumentationUrl}
>
<Link <Link
underline={false} underline={false}
footer="minor" footer="minor"
@@ -266,8 +271,8 @@ export const Footer = () => {
{t('links.accessibility')} {t('links.accessibility')}
</Link> </Link>
</StyledLi> </StyledLi>
{data?.diagnostics?.connection_test_enabled && ( {isConnectionTestEnabled && (
<StyledLi divider> <StyledLi divider={!!technicalDocumentationUrl}>
<Link <Link
underline={false} underline={false}
footer="minor" footer="minor"
@@ -278,19 +283,21 @@ export const Footer = () => {
</Link> </Link>
</StyledLi> </StyledLi>
)} )}
<StyledLi> {technicalDocumentationUrl && (
<A <StyledLi>
externalIcon <A
underline={false} externalIcon
footer="minor" underline={false}
href="https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/" footer="minor"
aria-label={ href={technicalDocumentationUrl}
t('links.technicalDetails') + ' - ' + t('links.ariaLabel') aria-label={
} t('links.technicalDetails') + ' - ' + t('links.ariaLabel')
> }
{t('links.technicalDetails')} >
</A> {t('links.technicalDetails')}
</StyledLi> </A>
</StyledLi>
)}
</SecondRow> </SecondRow>
<ThirdRow> <ThirdRow>
{t('mentions')}{' '} {t('mentions')}{' '}
@@ -29,11 +29,13 @@
"pending": "Ausstehend", "pending": "Ausstehend",
"running": "Läuft…", "running": "Läuft…",
"success": "Erfolgreich", "success": "Erfolgreich",
"warning": "Nicht optimal",
"failed": "Fehlgeschlagen", "failed": "Fehlgeschlagen",
"skipped": "Übersprungen" "skipped": "Übersprungen"
}, },
"counts": { "counts": {
"passed": "erfolgreich", "passed": "erfolgreich",
"warnings": "nicht optimal",
"failed": "fehlgeschlagen", "failed": "fehlgeschlagen",
"skipped": "übersprungen" "skipped": "übersprungen"
}, },
@@ -46,6 +48,10 @@
"passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.", "passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.",
"partial": "Teilweiser Test", "partial": "Teilweiser Test",
"partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.", "partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.",
"warning": "Verbindung nicht optimal",
"warningHint": "Sie können an Ihren Besprechungen teilnehmen, aber die Bild- und Tonqualität kann aufgrund Ihrer Netzwerkeinstellungen beeinträchtigt sein. Ihre IT-Abteilung kann hier Abhilfe schaffen.",
"warningDocLink": "Netzwerkanforderungen für Ihre IT-Abteilung",
"warningDocLinkAriaLabel": "Netzwerkanforderungen für Ihre IT-Abteilung öffnen – öffnet in neuem Tab",
"failed_one": "{{count}} Prüfung fehlgeschlagen", "failed_one": "{{count}} Prüfung fehlgeschlagen",
"failed_other": "{{count}} Prüfungen fehlgeschlagen", "failed_other": "{{count}} Prüfungen fehlgeschlagen",
"failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung." "failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung."
@@ -29,11 +29,13 @@
"pending": "Pending", "pending": "Pending",
"running": "Running…", "running": "Running…",
"success": "Passed", "success": "Passed",
"warning": "Not optimal",
"failed": "Failed", "failed": "Failed",
"skipped": "Skipped" "skipped": "Skipped"
}, },
"counts": { "counts": {
"passed": "passed", "passed": "passed",
"warnings": "not optimal",
"failed": "failed", "failed": "failed",
"skipped": "skipped" "skipped": "skipped"
}, },
@@ -46,6 +48,10 @@
"passedHint": "Your browser, your devices and your network are ready for a meeting.", "passedHint": "Your browser, your devices and your network are ready for a meeting.",
"partial": "Partially tested", "partial": "Partially tested",
"partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.", "partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.",
"warning": "Suboptimal connection",
"warningHint": "You can join your meetings, but video and audio quality may be reduced because of your network settings. Your IT department can improve this.",
"warningDocLink": "Network requirements for your IT department",
"warningDocLinkAriaLabel": "Open the network requirements for your IT department - opens in new window",
"failed_one": "{{count}} check failed", "failed_one": "{{count}} check failed",
"failed_other": "{{count}} checks failed", "failed_other": "{{count}} checks failed",
"failedHint": "Open the failed checks below for details, then send the report to your IT department." "failedHint": "Open the failed checks below for details, then send the report to your IT department."
@@ -29,11 +29,13 @@
"pending": "En espera", "pending": "En espera",
"running": "En curso…", "running": "En curso…",
"success": "Correcto", "success": "Correcto",
"warning": "No óptimo",
"failed": "Error", "failed": "Error",
"skipped": "Omitido" "skipped": "Omitido"
}, },
"counts": { "counts": {
"passed": "correctas", "passed": "correctas",
"warnings": "no óptimas",
"failed": "con errores", "failed": "con errores",
"skipped": "omitidas" "skipped": "omitidas"
}, },
@@ -46,6 +48,10 @@
"passedHint": "Tu navegador, tus dispositivos y tu red están listos para una reunión.", "passedHint": "Tu navegador, tus dispositivos y tu red están listos para una reunión.",
"partial": "Prueba parcial", "partial": "Prueba parcial",
"partialHint": "Se han omitido algunas comprobaciones. Autoriza el acceso a tu cámara y a tu micrófono para probarlos.", "partialHint": "Se han omitido algunas comprobaciones. Autoriza el acceso a tu cámara y a tu micrófono para probarlos.",
"warning": "Conexión no óptima",
"warningHint": "Puedes participar en tus reuniones, pero la calidad de la imagen y del sonido puede verse reducida por la configuración de tu red. Tu servicio informático puede mejorar la situación.",
"warningDocLink": "Requisitos de red para tu servicio informático",
"warningDocLinkAriaLabel": "Abrir los requisitos de red para tu servicio informático - se abre en una nueva ventana",
"failed_one": "{{count}} verificación en error", "failed_one": "{{count}} verificación en error",
"failed_other": "{{count}} verificaciones en error", "failed_other": "{{count}} verificaciones en error",
"failedHint": "Abre las verificaciones en error para ver el detalle y transmite después el informe a tu servicio informático." "failedHint": "Abre las verificaciones en error para ver el detalle y transmite después el informe a tu servicio informático."
@@ -29,11 +29,13 @@
"pending": "En attente", "pending": "En attente",
"running": "En cours…", "running": "En cours…",
"success": "Réussi", "success": "Réussi",
"warning": "Non optimal",
"failed": "Échec", "failed": "Échec",
"skipped": "Ignoré" "skipped": "Ignoré"
}, },
"counts": { "counts": {
"passed": "réussis", "passed": "réussis",
"warnings": "non optimaux",
"failed": "en échec", "failed": "en échec",
"skipped": "ignorés" "skipped": "ignorés"
}, },
@@ -46,6 +48,10 @@
"passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.", "passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.",
"partial": "Test partiel", "partial": "Test partiel",
"partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.", "partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.",
"warning": "Connexion non optimale",
"warningHint": "Vous pouvez participer à vos réunions, mais la qualité de l'image et du son risque d'être réduite à cause des réglages de votre réseau. Votre service informatique peut améliorer la situation.",
"warningDocLink": "Prérequis réseau à transmettre à votre service informatique",
"warningDocLinkAriaLabel": "Ouvrir les prérequis réseau à transmettre à votre service informatique - ouvre dans une nouvelle fenêtre",
"failed_one": "{{count}} vérification en échec", "failed_one": "{{count}} vérification en échec",
"failed_other": "{{count}} vérifications en échec", "failed_other": "{{count}} vérifications en échec",
"failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique." "failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique."
@@ -29,11 +29,13 @@
"pending": "In afwachting", "pending": "In afwachting",
"running": "Bezig…", "running": "Bezig…",
"success": "Geslaagd", "success": "Geslaagd",
"warning": "Niet optimaal",
"failed": "Mislukt", "failed": "Mislukt",
"skipped": "Overgeslagen" "skipped": "Overgeslagen"
}, },
"counts": { "counts": {
"passed": "geslaagd", "passed": "geslaagd",
"warnings": "niet optimaal",
"failed": "mislukt", "failed": "mislukt",
"skipped": "overgeslagen" "skipped": "overgeslagen"
}, },
@@ -46,6 +48,10 @@
"passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.", "passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.",
"partial": "Gedeeltelijke test", "partial": "Gedeeltelijke test",
"partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.", "partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.",
"warning": "Verbinding niet optimaal",
"warningHint": "Je kunt deelnemen aan je vergaderingen, maar de beeld- en geluidskwaliteit kan minder zijn door de instellingen van je netwerk. Je IT-afdeling kan dit verbeteren.",
"warningDocLink": "Netwerkvereisten voor je IT-afdeling",
"warningDocLinkAriaLabel": "Netwerkvereisten voor je IT-afdeling openen - opent in nieuw venster",
"failed_one": "{{count}} controle mislukt", "failed_one": "{{count}} controle mislukt",
"failed_other": "{{count}} controles mislukt", "failed_other": "{{count}} controles mislukt",
"failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling." "failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."
+1
View File
@@ -157,6 +157,7 @@ backend:
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}" FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}" FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}"
FRONTEND_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/7c5bd65d-3c21-486f-bce1-26e0a921d642/" FRONTEND_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/7c5bd65d-3c21-486f-bce1-26e0a921d642/"
FRONTEND_TECHNICAL_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/"
FRONTEND_MANIFEST_LINK: "https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/" FRONTEND_MANIFEST_LINK: "https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/"
FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000 FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr" FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"