Skip to content

Commit 74f5b76

Browse files
Implement internationalization with react-i18next
1 parent 0250eb2 commit 74f5b76

File tree

87 files changed

+778
-449
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+778
-449
lines changed

customWordList.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Videocam
66
issuetype
77
Autolinker
88
NOSONAR
9+
supportedLngs

frontend/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ VITE_BYPASS_WAITING_ROOM=false
1313
# For testing with multiple devices, you can use ngrok to expose your vite server to the internet.
1414
# Please refer to https://ngrok.com/docs/ for more information.
1515
VITE_TUNNEL_DOMAIN=''
16+
17+
# Default language for i18next
18+
VITE_I18N_FALLBACK_LANGUAGE='en'
19+
# List all supported language, separated by | (example: VITE_I18N_SUPPORTED_LANGUAGES='en|fr|de|it')
20+
VITE_I18N_SUPPORTED_LANGUAGES='en'

frontend/.eslintrc.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ module.exports = {
1818
'jsx-a11y/media-has-caption': 'off',
1919
'jsx-a11y/no-static-element-interactions': 'off',
2020
'jsx-a11y/click-events-have-key-events': 'off',
21-
'react/require-default-props': 'off'
21+
'react/require-default-props': 'off',
2222
},
2323
};

frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@
3636
"autoprefixer": "^10.4.19",
3737
"axios": "^1.12.0",
3838
"events": "^3.3.0",
39+
"i18next": "^25.3.2",
40+
"i18next-browser-languagedetector": "^8.2.0",
3941
"lodash": "^4.17.21",
4042
"opentok-layout-js": "^5.4.0",
4143
"opentok-solutions-logging": "^1.1.5",
4244
"postcss": "^8.4.38",
4345
"react": "^19.1.0",
4446
"react-dom": "^19.1.0",
47+
"react-i18next": "^15.6.1",
4548
"react-router-dom": "^6.11.0",
4649
"resize-observer-polyfill": "^1.5.1",
4750
"tailwindcss": "^3.4.14",

frontend/src/Context/PublisherProvider/usePublisher/usePublisher.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import useUserContext from '../../../hooks/useUserContext';
1212
import { UserContextType } from '../../user';
1313
import useSessionContext from '../../../hooks/useSessionContext';
1414
import { SessionContextType } from '../../SessionProvider/session';
15-
import { PUBLISHING_BLOCKED_CAPTION } from '../../../utils/constants';
1615

1716
vi.mock('@vonage/client-sdk-video');
1817
vi.mock('../../../hooks/useUserContext.tsx');
@@ -240,7 +239,8 @@ describe('usePublisher', () => {
240239

241240
const publishingBlockedError = {
242241
header: 'Difficulties joining room',
243-
caption: PUBLISHING_BLOCKED_CAPTION,
242+
caption:
243+
"We're having trouble connecting you with others in the meeting room. Please check your network and try again.",
244244
};
245245
expect(result.current.publishingError).toEqual(publishingBlockedError);
246246
expect(mockedSessionPublish).toHaveBeenCalledTimes(2);

frontend/src/Context/PublisherProvider/usePublisher/usePublisher.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@ import OT, {
77
ExceptionEvent,
88
PublisherProperties,
99
} from '@vonage/client-sdk-video';
10+
import { useTranslation } from 'react-i18next';
1011
import usePublisherQuality, { NetworkQuality } from '../usePublisherQuality/usePublisherQuality';
1112
import usePublisherOptions from '../usePublisherOptions';
1213
import useSessionContext from '../../../hooks/useSessionContext';
13-
import { PUBLISHING_BLOCKED_CAPTION } from '../../../utils/constants';
14-
import getAccessDeniedError, {
15-
PublishingErrorType,
16-
} from '../../../utils/getAccessDeniedError/getAccessDeniedError';
1714
import applyBackgroundFilter from '../../../utils/backgroundFilter/applyBackgroundFilter/applyBackgroundFilter';
1815

1916
type PublisherStreamCreatedEvent = Event<'streamCreated', Publisher> & {
@@ -29,6 +26,11 @@ type DeviceAccessStatus = {
2926
camera: boolean | undefined;
3027
};
3128

29+
type PublishingErrorType = {
30+
header: string;
31+
caption: string;
32+
} | null;
33+
3234
export type AccessDeniedEvent = Event<'accessDenied', Publisher> & {
3335
message?: string;
3436
};
@@ -72,6 +74,7 @@ export type PublisherContextType = {
7274
* @returns {PublisherContextType} the publisher context
7375
*/
7476
const usePublisher = (): PublisherContextType => {
77+
const { t } = useTranslation();
7578
const [publisherVideoElement, setPublisherVideoElement] = useState<
7679
HTMLVideoElement | HTMLObjectElement
7780
>();
@@ -96,10 +99,13 @@ const usePublisher = (): PublisherContextType => {
9699
useEffect(() => {
97100
if (deviceAccess?.microphone === false || deviceAccess?.camera === false) {
98101
const device = deviceAccess.camera ? 'Microphone' : 'Camera';
99-
const accessDeniedError = getAccessDeniedError(device);
102+
const accessDeniedError = {
103+
header: t('publishingErrors.accessDenied.title', { device }),
104+
caption: t('publishingErrors.accessDenied.message', { device: device.toLowerCase() }),
105+
};
100106
setPublishingError(accessDeniedError);
101107
}
102-
}, [deviceAccess]);
108+
}, [deviceAccess, t]);
103109

104110
useEffect(() => {
105111
if (!publisherOptions) {
@@ -232,8 +238,8 @@ const usePublisher = (): PublisherContextType => {
232238

233239
if (publishAttempt === 3) {
234240
const publishingBlocked: PublishingErrorType = {
235-
header: 'Difficulties joining room',
236-
caption: PUBLISHING_BLOCKED_CAPTION,
241+
header: t('publishingErrors.blocked.title'),
242+
caption: t('publishingErrors.blocked.message'),
237243
};
238244
setPublishingError(publishingBlocked);
239245
setIsPublishingToSession(false);

frontend/src/api/archiving/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ export type ArchiveResponse = {
88

99
/**
1010
* Returns a list of archives and the status of the archives for a given meeting room.
11+
* @param {string} locale - current locale
1112
* @param {string} roomName - The roomName we check for archives
1213
* @returns {Promise<ArchiveResponse>} The archives from the meeting room (if any) and whether any archives are pending.
1314
*/
14-
const getArchives = async (roomName: string): Promise<ArchiveResponse> => {
15+
const getArchives = async (locale: string, roomName: string): Promise<ArchiveResponse> => {
1516
const response = await listArchives(roomName);
1617
const archivesFromServer = response?.data?.archives;
1718
if (archivesFromServer instanceof Array) {
1819
const archives = archivesFromServer.map((archiveFromServer) =>
19-
createArchiveFromServer(archiveFromServer)
20+
createArchiveFromServer(locale, archiveFromServer)
2021
);
2122
return {
2223
archives,

frontend/src/api/archiving/model.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export type Archive = {
2727
createdAtFormatted: string;
2828
};
2929

30-
const getDateString = (timestamp: number) => {
31-
return `${getFormattedDate(timestamp)} ${getFormattedTime(timestamp)}`;
30+
const getDateString = (locale: string, timestamp: number) => {
31+
return `${getFormattedDate(locale, timestamp)} ${getFormattedTime(locale, timestamp)}`;
3232
};
3333

3434
const getArchiveStatus = (status: ServerArchiveStatus): ArchiveStatus => {
@@ -47,16 +47,17 @@ const getArchiveStatus = (status: ServerArchiveStatus): ArchiveStatus => {
4747

4848
/**
4949
* Modifies an archive retrieved from the server to be easily consumable.
50+
* @param {string} locale - current locale
5051
* @param {ServerArchive} serverArchive - The archive to be modified.
5152
* @returns {Archive} The modified archive.
5253
*/
53-
export const createArchiveFromServer = (serverArchive: ServerArchive): Archive => {
54+
export const createArchiveFromServer = (locale: string, serverArchive: ServerArchive): Archive => {
5455
return {
5556
id: serverArchive.id,
5657
url: serverArchive.url,
5758
status: getArchiveStatus(serverArchive.status),
5859
createdAt: serverArchive.createdAt,
59-
createdAtFormatted: getDateString(serverArchive.createdAt),
60+
createdAtFormatted: getDateString(locale, serverArchive.createdAt),
6061
};
6162
};
6263

frontend/src/api/archiving/tests/index.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const mockListArchives = listArchives as Mock<[], ReturnType<typeof listArchives
1010
describe('getArchives', () => {
1111
it('it returns an object with array of Archives and hasPending flag', async () => {
1212
mockListArchives.mockResolvedValue(mockResponse);
13-
const archives = await getArchives('roomName');
13+
const archives = await getArchives('en', 'roomName');
1414
expect(archives).toEqual({
1515
archives: [
1616
{
@@ -52,7 +52,7 @@ describe('getArchives', () => {
5252
},
5353
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5454
} as unknown as AxiosResponse<any, any>);
55-
const archives = await getArchives('roomName');
55+
const archives = await getArchives('en', 'roomName');
5656
expect(archives).toEqual({
5757
archives: [],
5858
hasPending: false,
@@ -61,6 +61,6 @@ describe('getArchives', () => {
6161

6262
it('it throws with error when api call throws', async () => {
6363
mockListArchives.mockRejectedValue(new AxiosError('Network Error', 'ERR_NETWORK'));
64-
expect(getArchives('roomName')).rejects.toThrowError();
64+
expect(getArchives('en', 'roomName')).rejects.toThrowError();
6565
});
6666
});

frontend/src/api/archiving/tests/model.spec.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { availableServerArchive, failedServerArchive, startedServerArchive } fro
44

55
describe('createArchiveFromServer', () => {
66
it('should convert fields to model fields', () => {
7-
const archive = createArchiveFromServer(availableServerArchive);
7+
const archive = createArchiveFromServer('en', availableServerArchive);
88
expect(archive).toEqual({
99
createdAt: 1725268141000,
1010
createdAtFormatted: 'Mon, Sep 2 5:09 AM',
@@ -15,27 +15,27 @@ describe('createArchiveFromServer', () => {
1515
});
1616

1717
it('should return status as failed for failed and expired archives', () => {
18-
expect(createArchiveFromServer(failedServerArchive).status).toBe('failed');
19-
expect(createArchiveFromServer({ ...failedServerArchive, status: 'expired' }).status).toBe(
20-
'failed'
21-
);
18+
expect(createArchiveFromServer('en', failedServerArchive).status).toBe('failed');
19+
expect(
20+
createArchiveFromServer('en', { ...failedServerArchive, status: 'expired' }).status
21+
).toBe('failed');
2222
});
2323

2424
it('should return available as failed for status available archives', () => {
25-
expect(createArchiveFromServer(availableServerArchive).status).toBe('available');
25+
expect(createArchiveFromServer('en', availableServerArchive).status).toBe('available');
2626
});
2727

2828
it('should return status as pending for started, stopped, uploaded, and paused archives', () => {
29-
expect(createArchiveFromServer(startedServerArchive).status).toBe('pending');
30-
expect(createArchiveFromServer({ ...startedServerArchive, status: 'stopped' }).status).toBe(
31-
'pending'
32-
);
33-
expect(createArchiveFromServer({ ...startedServerArchive, status: 'uploaded' }).status).toBe(
34-
'pending'
35-
);
36-
expect(createArchiveFromServer({ ...startedServerArchive, status: 'paused' }).status).toBe(
37-
'pending'
38-
);
29+
expect(createArchiveFromServer('en', startedServerArchive).status).toBe('pending');
30+
expect(
31+
createArchiveFromServer('en', { ...startedServerArchive, status: 'stopped' }).status
32+
).toBe('pending');
33+
expect(
34+
createArchiveFromServer('en', { ...startedServerArchive, status: 'uploaded' }).status
35+
).toBe('pending');
36+
expect(
37+
createArchiveFromServer('en', { ...startedServerArchive, status: 'paused' }).status
38+
).toBe('pending');
3939
});
4040
});
4141

0 commit comments

Comments
 (0)