(frontend) chose transcription’s language in settings

Add a key feature allowing users to choose the language
of their transcription via a setting.

The default value is set to French, the most commonly used
language across our user base.

Users can still select English or “Automatic,” which re-enables automatic
language detection if no default is configured on the microservice.
This commit is contained in:
lebaudantoine
2025-12-31 11:16:59 +01:00
committed by aleb_the_flash
parent 19f8c96e9d
commit 049a9079c4
15 changed files with 171 additions and 10 deletions

View File

@@ -13,7 +13,11 @@ import {
import { useEffect, useMemo, useState } from 'react'
import { ConnectionState, RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import {
RecordingLanguage,
RecordingStatus,
recordingStore,
} from '@/stores/recording'
import { FeatureFlags } from '@/features/analytics/enums'
import {
NotificationType,
@@ -31,6 +35,12 @@ import { LoginButton } from '@/components/LoginButton'
import { HStack, VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox.tsx'
import {
useSettingsDialog,
SettingsDialogExtendedKey,
useTranscriptionLanguageOptions,
} from '@/features/settings'
export const TranscriptSidePanel = () => {
const { data } = useConfig()
@@ -45,6 +55,9 @@ export const TranscriptSidePanel = () => {
const recordingSnap = useSnapshot(recordingStore)
const { notifyParticipants } = useNotifyParticipants()
const languageOptions = useTranscriptionLanguageOptions()
const { openSettingsDialog } = useSettingsDialog()
const hasTranscriptAccess = useHasRecordingAccess(
RecordingMode.Transcript,
@@ -115,7 +128,9 @@ export const TranscriptSidePanel = () => {
: RecordingMode.Transcript
const recordingOptions = {
language: 'fr', // fix hardcoded language
...(recordingSnap.language != RecordingLanguage.AUTOMATIC && {
language: recordingSnap.language,
}),
...(includeScreenRecording && { transcribe: true }),
}
@@ -459,9 +474,27 @@ export const TranscriptSidePanel = () => {
<div
className={css({
flex: 5,
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
})}
>
<Text variant="sm">{t('details.language')}</Text>
<Text variant="sm">
<Button
variant="text"
size="xs"
onPress={() =>
openSettingsDialog(SettingsDialogExtendedKey.TRANSCRIPTION)
}
>
{
languageOptions.find(
(option) => option.key == recordingSnap.language
)?.label
}
</Button>
</Text>
</div>
</div>

View File

@@ -16,6 +16,7 @@ import { NotificationsTab } from './tabs/NotificationsTab'
import { GeneralTab } from './tabs/GeneralTab'
import { AudioTab } from './tabs/AudioTab'
import { VideoTab } from './tabs/VideoTab'
import { TranscriptionTab } from './tabs/TranscriptionTab'
import { useRef } from 'react'
import { useMediaQuery } from '@/features/rooms/livekit/hooks/useMediaQuery'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
@@ -100,6 +101,11 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
{isWideScreen &&
t(`tabs.${SettingsDialogExtendedKey.NOTIFICATIONS}`)}
</Tab>
<Tab icon highlight id={SettingsDialogExtendedKey.TRANSCRIPTION}>
<span className="material-symbols">speech_to_text</span>
{isWideScreen &&
t(`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`)}
</Tab>
</TabList>
</div>
<div className={tabPanelContainerStyle}>
@@ -111,6 +117,8 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
<VideoTab id={SettingsDialogExtendedKey.VIDEO} />
<GeneralTab id={SettingsDialogExtendedKey.GENERAL} />
<NotificationsTab id={SettingsDialogExtendedKey.NOTIFICATIONS} />
{/* Transcription tab won't be accessible if the tab is not active in the tab list */}
<TranscriptionTab id={SettingsDialogExtendedKey.TRANSCRIPTION} />
</div>
</Tabs>
</Dialog>

View File

@@ -0,0 +1,30 @@
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { Field, H } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { RecordingLanguage, recordingStore } from '@/stores/recording'
import { useSnapshot } from 'valtio'
import { useTranscriptionLanguageOptions } from '../../hook/useTranscriptionLanguageOptions'
export type TranscriptionTabProps = Pick<TabPanelProps, 'id'>
export const TranscriptionTab = ({ id }: TranscriptionTabProps) => {
const { t } = useTranslation('settings', { keyPrefix: 'transcription' })
const recordingSnap = useSnapshot(recordingStore)
const languageOptions = useTranscriptionLanguageOptions()
return (
<TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('heading')}</H>
<Field
type="select"
label={t('language.label')}
items={languageOptions}
selectedKey={recordingSnap.language}
onSelectionChange={(lang) => {
recordingStore.language = lang as RecordingLanguage
}}
/>
</TabPanel>
)
}

View File

@@ -0,0 +1,28 @@
import { useMemo } from 'react'
import { RecordingLanguage } from '@/stores/recording'
import { useTranslation } from 'react-i18next'
export const useTranscriptionLanguageOptions = () => {
const { t } = useTranslation('settings', { keyPrefix: 'transcription' })
return useMemo(
() => [
{
key: RecordingLanguage.FRENCH,
value: RecordingLanguage.FRENCH,
label: t('language.options.french'),
},
{
key: RecordingLanguage.ENGLISH,
value: RecordingLanguage.ENGLISH,
label: t('language.options.english'),
},
{
key: RecordingLanguage.AUTOMATIC,
value: RecordingLanguage.AUTOMATIC,
label: t('language.options.auto'),
},
],
[t]
)
}

View File

@@ -1,2 +1,7 @@
export { SettingsButton } from './components/SettingsButton'
export { SettingsDialog } from './components/SettingsDialog'
export { useTranscriptionLanguageOptions } from './hook/useTranscriptionLanguageOptions'
export { useSettingsDialog } from './hook/useSettingsDialog'
export { SettingsDialogExtendedKey } from './type.ts'

View File

@@ -4,4 +4,5 @@ export enum SettingsDialogExtendedKey {
VIDEO = 'video',
GENERAL = 'general',
NOTIFICATIONS = 'notifications',
TRANSCRIPTION = 'transcription',
}

View File

@@ -317,7 +317,7 @@
"receiver": "Das Transkript wird an den Organisator und die Mitorganisatoren gesendet.",
"destination": "Ein neues Dokument wird erstellt auf",
"destinationUnknown": "Ein neues Dokument wird erstellt",
"language": "Meeting-Sprachen: Französisch (fr)",
"language": "Meeting-Sprache:",
"recording": "Auch eine Aufzeichnung starten"
},
"button": {

View File

@@ -68,6 +68,17 @@
},
"permissionsRequired": "Berechtigungen erforderlich"
},
"transcription": {
"heading": "Transkription",
"language": {
"label": "Besprechungssprache",
"options": {
"french": "Französisch (fr)",
"english": "Englisch (en)",
"auto": "Automatisch"
}
}
},
"notifications": {
"heading": "Tonbenachrichtigungen",
"label": "Tonbenachrichtigungen für",
@@ -100,6 +111,7 @@
"audio": "Audio",
"video": "Video",
"general": "Allgemein",
"notifications": "Benachrichtigungen"
"notifications": "Benachrichtigungen",
"transcription": "Transkription"
}
}

View File

@@ -317,7 +317,7 @@
"receiver": "The transcript will be sent to the host and co-hosts.",
"destination": "A new document will be created on",
"destinationUnknown": "A new document will be created",
"language": "Meeting language: French (fr)",
"language": "Meeting language:",
"recording": "Also start a recording"
},
"button": {

View File

@@ -68,6 +68,17 @@
},
"permissionsRequired": "Permissions required"
},
"transcription": {
"heading": "Transcription",
"language": {
"label": "Meeting language",
"options": {
"french": "French (fr)",
"english": "English (en)",
"auto": "Automatic"
}
}
},
"notifications": {
"heading": "Sound notifications",
"label": "sound notifications for",
@@ -100,6 +111,7 @@
"audio": "Audio",
"video": "Video",
"general": "General",
"notifications": "Notifications"
"notifications": "Notifications",
"transcription": "Transcription"
}
}

View File

@@ -317,7 +317,7 @@
"receiver": "La transcription sera envoyée à l'organisateur et aux coorganisateurs.",
"destination": "Un nouveau document sera créé sur",
"destinationUnknown": "Un nouveau document sera créé",
"language": "Langues de la réunion : Français (fr)",
"language": "Langue de la réunion :",
"recording": "Démarrer aussi un enregistrement"
},
"button": {

View File

@@ -68,6 +68,17 @@
},
"permissionsRequired": "Autorisations nécessaires"
},
"transcription": {
"heading": "Transcription",
"language": {
"label": "Langue de la réunion",
"options": {
"french": "Français (fr)",
"english": "Anglais (en)",
"auto": "Automatique"
}
}
},
"notifications": {
"heading": "Notifications sonores",
"label": "la notification sonore pour",
@@ -100,6 +111,7 @@
"audio": "Audio",
"video": "Vidéo",
"general": "Général",
"notifications": "Notifications"
"notifications": "Notifications",
"transcription": "Transcription"
}
}

View File

@@ -317,7 +317,7 @@
"receiver": "Het transcript wordt verzonden naar de organisator en medeorganisatoren.",
"destination": "Er wordt een nieuw document aangemaakt op",
"destinationUnknown": "Een nieuw document wordt aangemaakt",
"language": "Vergadertalen: Frans (fr)",
"language": "Vergadertalen:",
"recording": "Start ook een opname"
},
"button": {

View File

@@ -68,6 +68,17 @@
},
"permissionsRequired": "Machtigingen vereist"
},
"transcription": {
"heading": "Transcriptie",
"language": {
"label": "Vergadertaal",
"options": {
"french": "Frans (fr)",
"english": "Engels (en)",
"auto": "Automatisch"
}
}
},
"notifications": {
"heading": "Geluidsmeldingen",
"label": "Geluidsmeldingen voor",
@@ -100,6 +111,7 @@
"audio": "Audio",
"video": "Video",
"general": "Algemeen",
"notifications": "Meldingen"
"notifications": "Meldingen",
"transcription": "Transcriptie"
}
}

View File

@@ -1,5 +1,11 @@
import { proxy } from 'valtio'
export enum RecordingLanguage {
ENGLISH = 'en',
FRENCH = 'fr',
AUTOMATIC = 'auto',
}
export enum RecordingStatus {
TRANSCRIPT_STARTING,
TRANSCRIPT_STARTED,
@@ -13,8 +19,10 @@ export enum RecordingStatus {
type State = {
status: RecordingStatus
language: RecordingLanguage
}
export const recordingStore = proxy<State>({
status: RecordingStatus.STOPPED,
language: RecordingLanguage.FRENCH,
})