diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js index 71288d6ae551..e404a6fe7800 100644 --- a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js @@ -8,6 +8,7 @@ export default async function addUserPersistentData(user) { sortName: String, color: String, speechLocale: String, + translationLocale: String, mobile: Boolean, breakoutProps: Object, inactivityCheck: Boolean, diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js index 3d64c05d56f7..4f2344903136 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods.js +++ b/bigbluebutton-html5/imports/api/users/server/methods.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import validateAuthToken from './methods/validateAuthToken'; import setEmojiStatus from './methods/setEmojiStatus'; import setSpeechLocale from './methods/setSpeechLocale'; +import setTranslationLocale from './methods/setTranslationLocale'; import setMobileUser from './methods/setMobileUser'; import assignPresenter from './methods/assignPresenter'; import changeRole from './methods/changeRole'; @@ -17,6 +18,7 @@ import setExitReason from './methods/setExitReason'; Meteor.methods({ setEmojiStatus, setSpeechLocale, + setTranslationLocale, setMobileUser, assignPresenter, changeRole, diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setTranslationLocale.js b/bigbluebutton-html5/imports/api/users/server/methods/setTranslationLocale.js new file mode 100644 index 000000000000..4d1530326e45 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/setTranslationLocale.js @@ -0,0 +1,22 @@ +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import updateTranslationLocale from '../modifiers/updateTranslationLocale'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +const LANGUAGES = Meteor.settings.public.app.audioCaptions.language.available; + +export default function setTranslationLocale(locale) { + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + check(locale, String); + + if (LANGUAGES.includes(locale) || locale === '') { + updateTranslationLocale(meetingId, requesterUserId, locale); + } + } catch (err) { + Logger.error(`Exception while invoking method setTranslationLocale ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js index b8a476989bb1..35a31ee6f108 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js @@ -44,6 +44,7 @@ export default async function addUser(meetingId, userData) { meetingId, sortName: lowercaseTrim(user.name), speechLocale: '', + translationLocale: '', mobile: false, breakoutProps: { isBreakoutUser: Meeting.meetingProp.isBreakout, diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/updateTranslationLocale.js b/bigbluebutton-html5/imports/api/users/server/modifiers/updateTranslationLocale.js new file mode 100644 index 000000000000..f5bc7cab18a7 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/updateTranslationLocale.js @@ -0,0 +1,25 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; + +export default function updateTranslationLocale(meetingId, userId, locale) { + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + translationLocale: locale, + }, + }; + + try { + const numberAffected = Users.update(selector, modifier); + + if (numberAffected) { + Logger.info(`Updated translation locale=${locale} userId=${userId} meetingId=${meetingId}`); + } + } catch (err) { + Logger.error(`Updating translation locale: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index d0e1d262ff8a..89a134fd2f72 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -74,11 +74,7 @@ class ActionsBar extends PureComponent { ) : null} - { !deviceInfo.isMobile - ? ( - - ) - : null } + diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx index d87503540c56..72ad3b94dbb3 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx @@ -24,12 +24,22 @@ const intlMessages = defineMessages({ id: 'app.audio.captions.button.transcription', description: 'Audio speech transcription label', }, + translation: { + id: 'app.audio.captions.button.translation', + description: 'Audio speech translation label', + }, transcriptionOn: { id: 'app.switch.onLabel', }, transcriptionOff: { id: 'app.switch.offLabel', }, + translationOn: { + id: 'app.switch.onLabel', + }, + translationOff: { + id: 'app.switch.offLabel', + }, language: { id: 'app.audio.captions.button.language', description: 'Audio speech recognition language label', @@ -85,24 +95,39 @@ const CaptionsButton = ({ isRTL, enabled, currentSpeechLocale, + currentTranslationLocale, availableVoices, + availableTranslations, isSupported, isVoiceUser, }) => { const isTranscriptionDisabled = () => ( currentSpeechLocale === DISABLED ); + const isTranslationDisabled = () => ( + currentTranslationLocale === DISABLED + ); + + const isTranslationActivated = () => ( + Meteor.settings.public.captions.enableAutomaticTranslation + ); const fallbackLocale = availableVoices.includes(navigator.language) ? navigator.language : DEFAULT_LOCALE; const getSelectedLocaleValue = (isTranscriptionDisabled() ? fallbackLocale : currentSpeechLocale); + const getSelectedTranslationLocaleValue = (isTranslationDisabled() ? fallbackLocale : currentTranslationLocale); const selectedLocale = useRef(getSelectedLocaleValue); + const selectedTranslationLocale = useRef(getSelectedTranslationLocaleValue); useEffect(() => { if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue; }, [currentSpeechLocale]); + + useEffect(() => { + if (!isTranslationDisabled()) selectedTranslationLocale.current = getSelectedTranslationLocaleValue; + }, [currentTranslationLocale]); if (!enabled) return null; @@ -115,9 +140,8 @@ const CaptionsButton = ({ label: intl.formatMessage(intlMessages[availableVoice]), key: availableVoice, iconRight: selectedLocale.current === availableVoice ? 'check' : null, - customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel, + customStyles: (selectedLocale.current === availableVoice) ? Styled.SelectedLabel : Styled.NormalLabel, disabled: isTranscriptionDisabled(), - dividerTop: availableVoice === availableVoices[0], onClick: () => { selectedLocale.current = availableVoice; SpeechService.setSpeechLocale(selectedLocale.current); @@ -125,39 +149,79 @@ const CaptionsButton = ({ } )) ); + + const getAvailableTranslationLocales = () => ( + availableTranslations.map((availableTranslation) => ( + { + icon: '', + label: intl.formatMessage(intlMessages[availableTranslation]), + key: availableTranslation, + iconRight: selectedTranslationLocale.current === availableTranslation ? 'check' : null, + customStyles: (selectedTranslationLocale.current === availableTranslation) ? Styled.SelectedLabel : Styled.NormalLabel, + disabled: isTranscriptionDisabled() || isTranslationDisabled(), + onClick: () => { + selectedTranslationLocale.current = availableTranslation; + SpeechService.setTranslationLocale(selectedTranslationLocale.current); + }, + } + )) + ); const toggleTranscription = () => { SpeechService.setSpeechLocale(isTranscriptionDisabled() ? selectedLocale.current : DISABLED); }; - const getAvailableLocalesList = () => ( - [{ - key: 'availableLocalesList', - label: intl.formatMessage(intlMessages.language), - customStyles: Styled.TitleLabel, - disabled: true, - dividerTop: false, - }, - ...getAvailableLocales(), - { - key: 'divider', - label: intl.formatMessage(intlMessages.transcription), - customStyles: Styled.TitleLabel, - disabled: true, - }, { - key: 'transcriptionStatus', - label: intl.formatMessage( - isTranscriptionDisabled() - ? intlMessages.transcriptionOn - : intlMessages.transcriptionOff, - ), - customStyles: isTranscriptionDisabled() - ? Styled.EnableTrascription : Styled.DisableTrascription, - disabled: false, - dividerTop: true, - onClick: toggleTranscription, - }] - ); + const toggleTranslation = () => { + SpeechService.setTranslationLocale(isTranslationDisabled() ? selectedTranslationLocale.current : DISABLED); + }; + + const getAvailableLocalesList = () => { + let items = [{ + key: 'divider', + label: intl.formatMessage(intlMessages.transcription), + customStyles: Styled.TitleLabel, + disabled: true, + }, + ...getAvailableLocales(), + { + key: 'transcriptionStatus', + label: intl.formatMessage( + isTranscriptionDisabled() + ? intlMessages.transcriptionOn + : intlMessages.transcriptionOff, + ), + customStyles: isTranscriptionDisabled() + ? Styled.EnableTrascription : Styled.DisableTrascription, + disabled: false, + onClick: toggleTranscription, + }]; + + if (isTranslationActivated()) { + items = items.concat([ + { + key: 'divider', + label: intl.formatMessage(intlMessages.translation), + customStyles: Styled.TitleLabel, + disabled: true, + dividerTop: true, + }, + ...getAvailableTranslationLocales(), + { + key: 'translationStatus', + label: intl.formatMessage( + isTranslationDisabled() + ? intlMessages.translationOn + : intlMessages.translationOff, + ), + customStyles: isTranslationDisabled() + ? Styled.EnableTraslation : Styled.DisableTraslation, + disabled: isTranscriptionDisabled(), + onClick: toggleTranslation, + } + ]); + } + return items; + }; const onToggleClick = (e) => { e.stopPropagation(); @@ -219,6 +283,7 @@ CaptionsButton.propTypes = { isRTL: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired, currentSpeechLocale: PropTypes.string.isRequired, + currentTranslationLocale: PropTypes.string.isRequired, availableVoices: PropTypes.arrayOf(PropTypes.string).isRequired, isSupported: PropTypes.bool.isRequired, isVoiceUser: PropTypes.bool.isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx index 44a08b88c9a7..f9eb1caa14b8 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx @@ -10,7 +10,9 @@ const Container = (props) =>