diff --git a/README.md b/README.md index 2e49028e..f95511d3 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ This application provides features for common conferencing use cases, such as: Screenshot of noise supression toggle -
- Background effects in meeting and waiting room. You can set predefined images, custom image or slight/strong background blur. Images can be uploaded from local device or URL in these formats: JPG, PNG, GIF or BMP. + Background effects in meeting and waiting room. You can set predefined images, custom image or slight/strong background blur. Images can be uploaded from local device or URL in these formats: JPG, PNG, GIF or BMP. Background effects are not supported in non-Chromium-based browsers or on iOS. Screenshot of background effects
-
diff --git a/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.spec.tsx b/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.spec.tsx index 8377a411..203628e5 100644 --- a/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.spec.tsx +++ b/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.spec.tsx @@ -1,6 +1,31 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { vi, describe, it, expect, beforeAll } from 'vitest'; import AddBackgroundEffectLayout from './AddBackgroundEffectLayout'; +import enTranslations from '../../../../locales/en.json'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: Record) => { + const translations: Record = { + 'backgroundEffects.invalidFileType': enTranslations['backgroundEffects.invalidFileType'], + 'backgroundEffects.fileTooLarge': enTranslations['backgroundEffects.fileTooLarge'], + 'backgroundEffects.linkPlaceholder': enTranslations['backgroundEffects.linkPlaceholder'], + 'backgroundEffects.dragDropText': enTranslations['backgroundEffects.dragDropText'], + 'backgroundEffects.maxSize': enTranslations['backgroundEffects.maxSize'], + }; + + let translation = translations[key] || key; + + if (options && typeof translation === 'string') { + Object.keys(options).forEach((param) => { + translation = translation.replace(`{{${param}}}`, String(options[param])); + }); + } + + return translation; + }, + }), +})); vi.mock('../../../../utils/useImageStorage/useImageStorage', () => ({ __esModule: true, diff --git a/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.tsx b/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.tsx index 1e1d108f..1cde720d 100644 --- a/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.tsx +++ b/frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout.tsx @@ -9,6 +9,7 @@ import { import { ChangeEvent, ReactElement, useState } from 'react'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import LinkIcon from '@mui/icons-material/Link'; +import { useTranslation } from 'react-i18next'; import FileUploader from '../../FileUploader/FileUploader'; import { ALLOWED_TYPES, MAX_SIZE_MB } from '../../../../utils/constants'; import useImageStorage from '../../../../utils/useImageStorage/useImageStorage'; @@ -32,6 +33,7 @@ const AddBackgroundEffectLayout = ({ const [imageLink, setImageLink] = useState(''); const [linkLoading, setLinkLoading] = useState(false); const { storageError, handleImageFromFile, handleImageFromLink } = useImageStorage(); + const { t } = useTranslation(); type HandleFileChangeType = ChangeEvent | { target: { files: FileList } }; @@ -47,12 +49,12 @@ const AddBackgroundEffectLayout = ({ } if (!ALLOWED_TYPES.includes(file.type)) { - setFileError('Only JPG, PNG, GIF, or BMP images are allowed.'); + setFileError(t('backgroundEffects.invalidFileType')); return; } if (file.size > MAX_SIZE_MB * 1024 * 1024) { - setFileError(`Image must be less than ${MAX_SIZE_MB}MB.`); + setFileError(t('backgroundEffects.fileTooLarge', { maxSize: MAX_SIZE_MB })); return; } @@ -63,7 +65,7 @@ const AddBackgroundEffectLayout = ({ customBackgroundImageChange(newImage.dataUrl); } } catch { - setFileError('Failed to process uploaded image.'); + setFileError(t('backgroundEffects.processingError')); } }; @@ -76,7 +78,7 @@ const AddBackgroundEffectLayout = ({ setFileError(''); customBackgroundImageChange(newImage.dataUrl); } else { - setFileError('Failed to store image.'); + setFileError(t('backgroundEffects.storageError')); } } catch { // error handled in hook @@ -103,7 +105,7 @@ const AddBackgroundEffectLayout = ({ setImageLink(e.target.value)} diff --git a/frontend/src/components/BackgroundEffects/BackgroundEffectTabs/BackgroundEffectTabs.tsx b/frontend/src/components/BackgroundEffects/BackgroundEffectTabs/BackgroundEffectTabs.tsx index 6a1541e8..e6220b46 100644 --- a/frontend/src/components/BackgroundEffects/BackgroundEffectTabs/BackgroundEffectTabs.tsx +++ b/frontend/src/components/BackgroundEffects/BackgroundEffectTabs/BackgroundEffectTabs.tsx @@ -1,6 +1,7 @@ import { Box, Tabs, Tab } from '@mui/material'; import { Publisher } from '@vonage/client-sdk-video'; import { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; import EffectOptionButtons from '../EffectOptionButtons/EffectOptionButtons'; import BackgroundGallery from '../BackgroundGallery/BackgroundGallery'; import AddBackgroundEffectLayout from '../AddBackgroundEffect/AddBackgroundEffectLayout/AddBackgroundEffectLayout'; @@ -58,6 +59,7 @@ const BackgroundEffectTabs = ({ setBackgroundSelected(value); }; const isTabletViewport = useIsTabletViewport(); + const { t } = useTranslation(); return ( setTabSelected(newValue)} - aria-label="backgrounds tabs" + aria-label={t('backgroundEffects.title.tabs')} > - - + + diff --git a/frontend/src/components/BackgroundEffects/BackgroundEffectsLayout/BackgroundEffectsLayout.spec.tsx b/frontend/src/components/BackgroundEffects/BackgroundEffectsLayout/BackgroundEffectsLayout.spec.tsx index dc25fb33..23d10a3a 100644 --- a/frontend/src/components/BackgroundEffects/BackgroundEffectsLayout/BackgroundEffectsLayout.spec.tsx +++ b/frontend/src/components/BackgroundEffects/BackgroundEffectsLayout/BackgroundEffectsLayout.spec.tsx @@ -12,6 +12,9 @@ vi.mock('react-i18next', () => ({ const translations: Record = { 'backgroundEffects.title': enTranslations['backgroundEffects.title'], 'backgroundEffects.choice': enTranslations['backgroundEffects.choice'], + 'backgroundEffects.tabs.backgrounds': enTranslations['backgroundEffects.tabs.backgrounds'], + 'backgroundEffects.tabs.addBackground': + enTranslations['backgroundEffects.tabs.addBackground'], 'button.cancel': enTranslations['button.cancel'], 'button.apply': enTranslations['button.apply'], }; diff --git a/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.spec.tsx b/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.spec.tsx index 43baa6a4..1e0385bd 100644 --- a/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.spec.tsx +++ b/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.spec.tsx @@ -1,13 +1,29 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import BackgroundGallery, { backgrounds } from './BackgroundGallery'; +import BackgroundGallery from './BackgroundGallery'; +import enTranslations from '../../../locales/en.json'; const customImages = [ { id: 'custom1', dataUrl: 'data:image/png;base64,custom1' }, { id: 'custom2', dataUrl: 'data:image/png;base64,custom2' }, ]; +const backgrounds = [ + { + id: 'bg4', + file: 'hogwarts.jpg', + name: enTranslations['backgroundEffects.backgrounds.hogwarts'], + }, + { id: 'bg5', file: 'library.jpg', name: enTranslations['backgroundEffects.backgrounds.library'] }, + { + id: 'bg6', + file: 'new-york.jpg', + name: enTranslations['backgroundEffects.backgrounds.newYork'], + }, + { id: 'bg7', file: 'plane.jpg', name: enTranslations['backgroundEffects.backgrounds.plane'] }, +]; + const mockDeleteImageFromStorage = vi.fn(); const mockGetImagesFromStorage = vi.fn(() => customImages); diff --git a/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.tsx b/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.tsx index bb5d4a84..9f0adfd1 100644 --- a/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.tsx +++ b/frontend/src/components/BackgroundEffects/BackgroundGallery/BackgroundGallery.tsx @@ -1,21 +1,11 @@ import { ReactElement, useEffect, useState } from 'react'; import { Box, IconButton, Tooltip } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; +import { useTranslation } from 'react-i18next'; import { BACKGROUNDS_PATH } from '../../../utils/constants'; import SelectableOption from '../SelectableOption'; import useImageStorage, { StoredImage } from '../../../utils/useImageStorage/useImageStorage'; -export const backgrounds = [ - { id: 'bg1', file: 'bookshelf-room.jpg', name: 'Bookshelf Room' }, - { id: 'bg2', file: 'busy-room.jpg', name: 'Busy Room' }, - { id: 'bg3', file: 'dune-view.jpg', name: 'Dune View' }, - { id: 'bg4', file: 'hogwarts.jpg', name: 'Hogwarts' }, - { id: 'bg5', file: 'library.jpg', name: 'Library' }, - { id: 'bg6', file: 'new-york.jpg', name: 'New York' }, - { id: 'bg7', file: 'plane.jpg', name: 'Plane' }, - { id: 'bg8', file: 'white-room.jpg', name: 'White Room' }, -]; - export type BackgroundGalleryProps = { backgroundSelected: string; setBackgroundSelected: (dataUrl: string) => void; @@ -39,6 +29,22 @@ const BackgroundGallery = ({ }: BackgroundGalleryProps): ReactElement => { const { getImagesFromStorage, deleteImageFromStorage } = useImageStorage(); const [customImages, setCustomImages] = useState([]); + const { t } = useTranslation(); + + const backgrounds = [ + { + id: 'bg1', + file: 'bookshelf-room.jpg', + name: t('backgroundEffects.backgrounds.bookshelfRoom'), + }, + { id: 'bg2', file: 'busy-room.jpg', name: t('backgroundEffects.backgrounds.busyRoom') }, + { id: 'bg3', file: 'dune-view.jpg', name: t('backgroundEffects.backgrounds.duneView') }, + { id: 'bg4', file: 'hogwarts.jpg', name: t('backgroundEffects.backgrounds.hogwarts') }, + { id: 'bg5', file: 'library.jpg', name: t('backgroundEffects.backgrounds.library') }, + { id: 'bg6', file: 'new-york.jpg', name: t('backgroundEffects.backgrounds.newYork') }, + { id: 'bg7', file: 'plane.jpg', name: t('backgroundEffects.backgrounds.plane') }, + { id: 'bg8', file: 'white-room.jpg', name: t('backgroundEffects.backgrounds.whiteRoom') }, + ]; useEffect(() => { setCustomImages(getImagesFromStorage()); @@ -67,7 +73,7 @@ const BackgroundGallery = ({ > setBackgroundSelected(dataUrl)} image={dataUrl} @@ -75,14 +81,14 @@ const BackgroundGallery = ({ { e.stopPropagation(); if (!isSelected) { diff --git a/frontend/src/components/BackgroundEffects/EffectOptionButtons/EffectOptionButtons.tsx b/frontend/src/components/BackgroundEffects/EffectOptionButtons/EffectOptionButtons.tsx index 55d0dea2..3a9e6084 100644 --- a/frontend/src/components/BackgroundEffects/EffectOptionButtons/EffectOptionButtons.tsx +++ b/frontend/src/components/BackgroundEffects/EffectOptionButtons/EffectOptionButtons.tsx @@ -1,18 +1,9 @@ import { ReactElement } from 'react'; import BlockIcon from '@mui/icons-material/Block'; import BlurOnIcon from '@mui/icons-material/BlurOn'; +import { useTranslation } from 'react-i18next'; import SelectableOption from '../SelectableOption'; -const options = [ - { key: 'none', icon: , name: 'Remove background' }, - { key: 'low-blur', icon: , name: 'Slight background blur' }, - { - key: 'high-blur', - icon: , - name: 'Strong background blur', - }, -]; - export type EffectOptionButtonsProps = { backgroundSelected: string; setBackgroundSelected: (key: string) => void; @@ -31,6 +22,20 @@ const EffectOptionButtons = ({ backgroundSelected, setBackgroundSelected, }: EffectOptionButtonsProps): ReactElement => { + const { t } = useTranslation(); + const options = [ + { + key: 'none', + icon: , + name: t('backgroundEffects.removeBackground'), + }, + { key: 'low-blur', icon: , name: t('backgroundEffects.slightBlur') }, + { + key: 'high-blur', + icon: , + name: t('backgroundEffects.strongBlur'), + }, + ]; return ( <> {options.map(({ key, icon, name }) => ( diff --git a/frontend/src/components/BackgroundEffects/FileUploader/FileUploader.tsx b/frontend/src/components/BackgroundEffects/FileUploader/FileUploader.tsx index 188b1fe4..98bc099c 100644 --- a/frontend/src/components/BackgroundEffects/FileUploader/FileUploader.tsx +++ b/frontend/src/components/BackgroundEffects/FileUploader/FileUploader.tsx @@ -1,6 +1,7 @@ import { ChangeEvent, useState, DragEvent, ReactElement } from 'react'; import { Box, Typography } from '@mui/material'; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import { useTranslation } from 'react-i18next'; import { MAX_SIZE_MB } from '../../../utils/constants'; export type FileUploaderProps = { @@ -19,6 +20,7 @@ export type FileUploaderProps = { */ const FileUploader = ({ handleFileChange }: FileUploaderProps): ReactElement => { const [dragOver, setDragOver] = useState(false); + const { t } = useTranslation(); const onDragOver = (e: DragEvent) => { e.preventDefault(); @@ -69,9 +71,9 @@ const FileUploader = ({ handleFileChange }: FileUploaderProps): ReactElement => <> - Drag and drop, or click here to upload image, + {t('backgroundEffects.dragDropText')}
- Max {MAX_SIZE_MB}MB + {t('backgroundEffects.maxSize', { maxSize: MAX_SIZE_MB })}
diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx index bf7ee28f..66099d9c 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.spec.tsx @@ -9,6 +9,7 @@ import usePublisherContext from '../../../hooks/usePublisherContext'; import DeviceControlButton from './DeviceControlButton'; import useConfigContext from '../../../hooks/useConfigContext'; import { ConfigContextType } from '../../../Context/ConfigProvider'; +import enTranslations from '../../../locales/en.json'; vi.mock('../../../hooks/usePublisherContext.tsx'); vi.mock('../../../hooks/useSpeakingDetector.tsx'); @@ -22,6 +23,22 @@ vi.mock('../../../hooks/useBackgroundPublisherContext', () => { }); vi.mock('../../../hooks/useConfigContext'); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'devices.audio.microphone.full': enTranslations['devices.audio.microphone.full'], + 'devices.video.camera.full': enTranslations['devices.video.camera.full'], + 'devices.settings.ariaLabel': enTranslations['devices.settings.ariaLabel'], + 'devices.video.disabled': enTranslations['devices.video.disabled'], + 'devices.audio.disabled': enTranslations['devices.audio.disabled'], + 'mutedAlert.message.muted': enTranslations['mutedAlert.message.muted'], + }; + return translations[key] || key; + }, + }), +})); + const mockUsePublisherContext = usePublisherContext as Mock<[], PublisherContextType>; const mockUseSpeakingDetector = useSpeakingDetector as Mock<[], boolean>; const mockHandleToggleBackgroundEffects = vi.fn(); @@ -103,7 +120,7 @@ describe('DeviceControlButton', () => { toggleBackgroundEffects={mockHandleToggleBackgroundEffects} /> ); - const cameraButton = screen.getByLabelText('camera'); + const cameraButton = screen.getByLabelText('Camera'); cameraButton.click(); expect(toggleVideoMock).toHaveBeenCalled(); expect(toggleBackgroundVideoPublisherMock).toHaveBeenCalled(); @@ -117,7 +134,7 @@ describe('DeviceControlButton', () => { toggleBackgroundEffects={mockHandleToggleBackgroundEffects} /> ); - const micButton = screen.getByLabelText('microphone'); + const micButton = screen.getByLabelText('Microphone'); expect(micButton).toBeInTheDocument(); expect(micButton).not.toBeDisabled(); @@ -132,7 +149,7 @@ describe('DeviceControlButton', () => { toggleBackgroundEffects={mockHandleToggleBackgroundEffects} /> ); - const micButton = screen.getByLabelText('microphone'); + const micButton = screen.getByLabelText('Microphone'); expect(micButton).toBeInTheDocument(); expect(micButton).toBeDisabled(); @@ -153,7 +170,7 @@ describe('DeviceControlButton', () => { /> ); - const videoButton = screen.getByLabelText('camera'); + const videoButton = screen.getByLabelText('Camera'); expect(videoButton).toBeInTheDocument(); expect(videoButton).not.toBeDisabled(); @@ -169,7 +186,7 @@ describe('DeviceControlButton', () => { /> ); - const videoButton = screen.getByLabelText('camera'); + const videoButton = screen.getByLabelText('Camera'); expect(videoButton).toBeInTheDocument(); expect(videoButton).toBeDisabled(); diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx index 26100e79..3f90ed4c 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx @@ -127,7 +127,9 @@ const DeviceControlButton = ({ onClick={handleDeviceStateChange} disabled={isButtonDisabled} edge="start" - aria-label={isAudio ? 'microphone' : 'camera'} + aria-label={ + isAudio ? t('devices.audio.microphone.full') : t('devices.video.camera.full') + } size="small" className="m-[3px] size-[50px] rounded-full shadow-md" > diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx index 52e70ac6..6aee4ad1 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.spec.tsx @@ -346,7 +346,7 @@ describe('DeviceSettingsMenu Component', () => { await waitFor(() => { expect(screen.queryByTestId('dropdown-separator')).toBeVisible(); - expect(screen.queryByText('Background effects')).toBeVisible(); + expect(screen.queryByText('Background Effects')).toBeVisible(); }); }); @@ -365,7 +365,7 @@ describe('DeviceSettingsMenu Component', () => { await waitFor(() => { expect(screen.queryByTestId('dropdown-separator')).not.toBeInTheDocument(); - expect(screen.queryByText('Background effects')).not.toBeInTheDocument(); + expect(screen.queryByText('Background Effects')).not.toBeInTheDocument(); }); }); @@ -394,7 +394,7 @@ describe('DeviceSettingsMenu Component', () => { await waitFor(() => { expect(screen.queryByTestId('dropdown-separator')).not.toBeInTheDocument(); - expect(screen.queryByText('Background effects')).not.toBeInTheDocument(); + expect(screen.queryByText('Background Effects')).not.toBeInTheDocument(); }); }); }); diff --git a/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.spec.tsx b/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.spec.tsx index 18a36020..da66c024 100644 --- a/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.spec.tsx +++ b/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.spec.tsx @@ -1,6 +1,18 @@ import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import VideoDevicesOptions from './VideoDevicesOptions'; +import enTranslations from '../../../locales/en.json'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'backgroundEffects.title': enTranslations['backgroundEffects.title'], + }; + return translations[key] || key; + }, + }), +})); describe('VideoDevicesOptions', () => { const toggleBackgroundEffects = vi.fn(); @@ -15,7 +27,7 @@ describe('VideoDevicesOptions', () => { it('renders the background effects menu item', () => { render(); - expect(screen.getByTestId('background-effects-text')).toHaveTextContent('Background effects'); + expect(screen.getByTestId('background-effects-text')).toHaveTextContent('Background Effects'); expect(screen.getByRole('menuitem')).toBeInTheDocument(); }); diff --git a/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx b/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx index 762ca2c2..f7c34c87 100644 --- a/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx +++ b/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx @@ -1,6 +1,7 @@ import { Typography, MenuList, MenuItem } from '@mui/material'; import { ReactElement } from 'react'; import PortraitIcon from '@mui/icons-material/Portrait'; +import { useTranslation } from 'react-i18next'; export type VideoDevicesOptionsProps = { toggleBackgroundEffects: () => void; @@ -17,6 +18,8 @@ export type VideoDevicesOptionsProps = { const VideoDevicesOptions = ({ toggleBackgroundEffects, }: VideoDevicesOptionsProps): ReactElement => { + const { t } = useTranslation(); + return ( - Background effects + {t('backgroundEffects.title')} diff --git a/frontend/src/components/WaitingRoom/BackgroundEffects/BackgroundEffectsButton/BackgroundEffectsButton.tsx b/frontend/src/components/WaitingRoom/BackgroundEffects/BackgroundEffectsButton/BackgroundEffectsButton.tsx index 90632e24..821542b9 100644 --- a/frontend/src/components/WaitingRoom/BackgroundEffects/BackgroundEffectsButton/BackgroundEffectsButton.tsx +++ b/frontend/src/components/WaitingRoom/BackgroundEffects/BackgroundEffectsButton/BackgroundEffectsButton.tsx @@ -2,6 +2,7 @@ import { Box, Tooltip } from '@mui/material'; import { hasMediaProcessorSupport } from '@vonage/client-sdk-video'; import { ReactElement } from 'react'; import PortraitIcon from '@mui/icons-material/Portrait'; +import { useTranslation } from 'react-i18next'; import VideoContainerButton from '../../VideoContainerButton'; import useConfigContext from '../../../../hooks/useConfigContext'; @@ -23,6 +24,7 @@ const BackgroundEffectsButton = ({ const config = useConfigContext(); const { allowBackgroundEffects } = config.videoSettings; const shouldDisplayBackgroundEffects = hasMediaProcessorSupport() && allowBackgroundEffects; + const { t } = useTranslation(); return ( shouldDisplayBackgroundEffects && ( @@ -40,7 +42,7 @@ const BackgroundEffectsButton = ({ transition: 'transform 0.2s ease-in-out', }} > - + { setIsBackgroundEffectsOpen(false); }; + const { t } = useTranslation(); return ( - Background Effects + {t('backgroundEffects.title')} { const [storageError, setStorageError] = useState(''); + const { t } = useTranslation(); // Estimate size of a string in bytes const estimateSizeInBytes = (str: string) => new Blob([str]).size; @@ -42,14 +44,14 @@ const useImageStorage = () => { try { const totalSize = images.reduce((acc, img) => acc + estimateSizeInBytes(img.dataUrl), 0); if (totalSize > MAX_LOCAL_STORAGE_BYTES) { - setStorageError('Images are too large to store (~4MB max).'); + setStorageError(t('imageStorage.imagesExceedSize')); return false; } setStorageItem(STORAGE_KEYS.BACKGROUND_IMAGE, JSON.stringify(images)); setStorageError(''); return true; } catch { - setStorageError('Failed to store images in localStorage.'); + setStorageError(t('imageStorage.storageFailure')); return false; } }; @@ -64,7 +66,7 @@ const useImageStorage = () => { const isDuplicate = images.some((img) => img.dataUrl === dataUrl); if (isDuplicate) { - setStorageError('This image is already added.'); + setStorageError(t('imageStorage.duplicateImage')); return null; } @@ -110,7 +112,7 @@ const useImageStorage = () => { } }; reader.onerror = () => { - setStorageError('Failed to read image file.'); + setStorageError(t('imageStorage.fileReadError')); reject(); }; reader.readAsDataURL(file); @@ -128,12 +130,12 @@ const useImageStorage = () => { const parsed = new URL(url); const validExt = /\.(jpg|jpeg|png|gif|bmp)$/i.test(parsed.pathname); if (!validExt) { - setStorageError('Invalid image extension.'); + setStorageError(t('imageStorage.invalidExtension')); reject(); return; } } catch { - setStorageError('Invalid image URL.'); + setStorageError(t('imageStorage.invalidUrl')); reject(); return; } @@ -155,12 +157,12 @@ const useImageStorage = () => { reject(); } } catch { - setStorageError('Could not convert image.'); + setStorageError(t('imageStorage.convertError')); reject(); } }; img.onerror = () => { - setStorageError('Could not load image.'); + setStorageError(t('imageStorage.loadError')); reject(); }; img.src = url;