Skip to content

Commit 38639c9

Browse files
committed
✨(frontend) sync user and frontend language
On Language change in the frontend, the user language is updated via API. If user language is available, it will be preferred and set in the frontend.
1 parent 6c9e185 commit 38639c9

File tree

17 files changed

+240
-150
lines changed

17 files changed

+240
-150
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to
1313

1414
## Changed
1515

16+
- ✨(frontend) sync user and frontend language #401
17+
1618
## Fixed
1719

1820
- 🐛(backend) invitation e-mails in receivers language #401

src/backend/core/api/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class UserSerializer(serializers.ModelSerializer):
1818

1919
class Meta:
2020
model = models.User
21-
fields = ["id", "email", "full_name", "short_name"]
21+
fields = ["id", "email", "full_name", "short_name", "language"]
2222
read_only_fields = ["id", "email", "full_name", "short_name"]
2323

2424

src/backend/demo/defaults.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,12 @@
77
}
88

99
DEV_USERS = [
10+
{"username": "impress", "email": "impress@impress.world", "language": "en-us"},
11+
{"username": "user-e2e-webkit", "email": "user@webkit.e2e", "language": "en-us"},
12+
{"username": "user-e2e-firefox", "email": "user@firefox.e2e", "language": "en-us"},
1013
{
11-
"username": "impress",
12-
"email": "impress@impress.world",
14+
"username": "user-e2e-chromium",
15+
"email": "user@chromium.e2e",
16+
"language": "en-us",
1317
},
14-
{
15-
"username": "user-e2e-webkit",
16-
"email": "user@webkit.e2e",
17-
},
18-
{
19-
"username": "user-e2e-firefox",
20-
"email": "user@firefox.e2e",
21-
},
22-
{"username": "user-e2e-chromium", "email": "user@chromium.e2e"},
2318
]

src/backend/demo/management/commands/create_demo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ def create_demo(stdout):
172172
is_superuser=False,
173173
is_active=True,
174174
is_staff=False,
175-
language=random.choice(settings.LANGUAGES)[0],
175+
language=dev_user["language"]
176+
or random.choice(settings.LANGUAGES)[0],
176177
)
177178
)
178179

src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,6 @@ test.beforeEach(async ({ page }) => {
99
});
1010

1111
test.describe('Doc Editor', () => {
12-
test('it check translations of the slash menu when changing language', async ({
13-
page,
14-
browserName,
15-
}) => {
16-
await createDoc(page, 'doc-toolbar', browserName, 1);
17-
18-
const header = page.locator('header').first();
19-
const editor = page.locator('.ProseMirror');
20-
// Trigger slash menu to show english menu
21-
await editor.click();
22-
await editor.fill('/');
23-
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
24-
await header.click();
25-
await expect(page.getByText('Headings', { exact: true })).toBeHidden();
26-
27-
// Reset menu
28-
await editor.click();
29-
await editor.fill('');
30-
31-
// Change language to French
32-
await header.click();
33-
await header.getByRole('combobox').getByText('English').click();
34-
await header.getByRole('option', { name: 'Français' }).click();
35-
await expect(
36-
header.getByRole('combobox').getByText('Français'),
37-
).toBeVisible();
38-
39-
// Trigger slash menu to show french menu
40-
await editor.click();
41-
await editor.fill('/');
42-
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
43-
await header.click();
44-
await expect(page.getByText('Titres', { exact: true })).toBeHidden();
45-
});
46-
4712
test('it checks default toolbar buttons are displayed', async ({
4813
page,
4914
browserName,

src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,13 @@ test.describe('Document create member', () => {
111111
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
112112
const responseCreateInvitation = await responsePromiseCreateInvitation;
113113
expect(responseCreateInvitation.ok()).toBeTruthy();
114-
expect(
115-
responseCreateInvitation.request().headers()['content-language'],
116-
).toBe('en-us');
117114

118115
// Check user added
119116
await expect(
120117
page.getByText(`User ${user.email} added to the document.`),
121118
).toBeVisible();
122119
const responseAddUser = await responsePromiseAddUser;
123120
expect(responseAddUser.ok()).toBeTruthy();
124-
expect(responseAddUser.request().headers()['content-language']).toBe(
125-
'en-us',
126-
);
127121

128122
const listInvitation = page.getByLabel('List invitation card');
129123
await expect(listInvitation.locator('li').getByText(email)).toBeVisible();
@@ -225,46 +219,6 @@ test.describe('Document create member', () => {
225219
expect(responseCreateInvitationFail.ok()).toBeFalsy();
226220
});
227221

228-
test('The invitation endpoint get the language of the website', async ({
229-
page,
230-
browserName,
231-
}) => {
232-
await createDoc(page, 'user-invitation', browserName, 1);
233-
234-
const header = page.locator('header').first();
235-
await header.getByRole('combobox').getByText('EN').click();
236-
await header.getByRole('option', { name: 'FR' }).click();
237-
238-
await page.getByRole('button', { name: 'Partager' }).click();
239-
240-
const inputSearch = page.getByLabel(
241-
/Trouver un membre à ajouter au document/,
242-
);
243-
244-
const email = randomName('test@test.fr', browserName, 1)[0];
245-
await inputSearch.fill(email);
246-
await page.getByRole('option', { name: email }).click();
247-
248-
// Choose a role
249-
await page.getByRole('combobox', { name: /Choisissez un rôle/ }).click();
250-
await page.getByRole('option', { name: 'Administrateur' }).click();
251-
252-
const responsePromiseCreateInvitation = page.waitForResponse(
253-
(response) =>
254-
response.url().includes('/invitations/') && response.status() === 201,
255-
);
256-
257-
await page.getByRole('button', { name: 'Valider' }).click();
258-
259-
// Check invitation sent
260-
await expect(page.getByText(`Invitation envoyée à ${email}`)).toBeVisible();
261-
const responseCreateInvitation = await responsePromiseCreateInvitation;
262-
expect(responseCreateInvitation.ok()).toBeTruthy();
263-
expect(
264-
responseCreateInvitation.request().headers()['content-language'],
265-
).toBe('fr-fr');
266-
});
267-
268222
test('it manages invitation', async ({ page, browserName }) => {
269223
await createDoc(page, 'user-invitation', browserName, 1);
270224

src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
import { expect, test } from '@playwright/test';
1+
import { Page, expect, test } from '@playwright/test';
2+
3+
import { createDoc } from './common';
24

35
test.beforeEach(async ({ page }) => {
46
await page.goto('/');
57
});
68

79
test.describe('Language', () => {
8-
test('checks the language picker', async ({ page }) => {
10+
test('checks language switching', async ({ page }) => {
11+
const header = page.locator('header').first();
12+
13+
// initial language should be english
914
await expect(
1015
page.getByRole('button', {
1116
name: 'Create a new document',
1217
}),
1318
).toBeVisible();
1419

15-
const header = page.locator('header').first();
16-
await header.getByRole('combobox').getByText('English').click();
17-
await header.getByRole('option', { name: 'Français' }).click();
20+
// switch to french
21+
await waitForLanguageSwitch(page, TestLanguage.French);
22+
1823
await expect(
1924
header.getByRole('combobox').getByText('Français'),
2025
).toBeVisible();
@@ -63,12 +68,79 @@ test.describe('Language', () => {
6368
// Check for English 404 response
6469
await check404Response('Not found.');
6570

66-
// Switch language to French
67-
const header = page.locator('header').first();
68-
await header.getByRole('combobox').getByText('English').click();
69-
await header.getByRole('option', { name: 'Français' }).click();
71+
await waitForLanguageSwitch(page, TestLanguage.French);
7072

7173
// Check for French 404 response
7274
await check404Response('Pas trouvé.');
7375
});
76+
77+
test('it check translations of the slash menu when changing language', async ({
78+
page,
79+
browserName,
80+
}) => {
81+
await createDoc(page, 'doc-toolbar', browserName, 1);
82+
83+
const header = page.locator('header').first();
84+
const editor = page.locator('.ProseMirror');
85+
// Trigger slash menu to show english menu
86+
await editor.click();
87+
await editor.fill('/');
88+
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
89+
await header.click();
90+
await expect(page.getByText('Headings', { exact: true })).toBeHidden();
91+
92+
// Reset menu
93+
await editor.click();
94+
await editor.fill('');
95+
96+
// Change language to French
97+
await waitForLanguageSwitch(page, TestLanguage.French);
98+
99+
// Trigger slash menu to show french menu
100+
await editor.click();
101+
await editor.fill('/');
102+
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
103+
await header.click();
104+
await expect(page.getByText('Titres', { exact: true })).toBeHidden();
105+
});
74106
});
107+
108+
test.afterEach(async ({ page }) => {
109+
// Switch back to English - important for other tests to run as expected
110+
await waitForLanguageSwitch(page, TestLanguage.English);
111+
});
112+
113+
// language helper
114+
export const TestLanguage = {
115+
English: {
116+
label: 'English',
117+
expectedLocale: ['en-us'],
118+
},
119+
French: {
120+
label: 'Français',
121+
expectedLocale: ['fr-fr'],
122+
},
123+
} as const;
124+
125+
type TestLanguageKey = keyof typeof TestLanguage;
126+
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
127+
128+
export async function waitForLanguageSwitch(
129+
page: Page,
130+
lang: TestLanguageValue,
131+
) {
132+
const header = page.locator('header').first();
133+
await header.getByRole('combobox').click();
134+
135+
const [response] = await Promise.all([
136+
page.waitForResponse(
137+
(resp) =>
138+
resp.url().includes('/user') && resp.request().method() === 'PATCH',
139+
),
140+
header.getByRole('option', { name: lang.label }).click(),
141+
]);
142+
143+
const updatedUserResponse = await response.json();
144+
145+
expect(lang.expectedLocale).toContain(updatedUserResponse.language);
146+
}

src/frontend/apps/impress/src/core/AppProvider.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
33
import { useEffect } from 'react';
44

55
import { useCunninghamTheme } from '@/cunningham';
6-
import '@/i18n/initI18n';
76
import { useResponsiveStore } from '@/stores/';
87

98
import { Auth } from './auth/';

src/frontend/apps/impress/src/core/auth/api/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
* @property {string} id - The id of the user.
55
* @property {string} email - The email of the user.
66
* @property {string} name - The name of the user.
7+
* @property {string} language - The language of the user.
78
*/
89
export interface User {
910
id: string;
1011
email: string;
1112
full_name: string;
1213
short_name: string;
14+
language: string;
1315
}

src/frontend/apps/impress/src/core/config/ConfigProvider.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import { PropsWithChildren, useEffect } from 'react';
33

44
import { Box } from '@/components';
55
import { useCunninghamTheme } from '@/cunningham';
6+
import i18n from '@/i18n/initI18n';
67
import { configureCrispSession } from '@/services';
78
import { useSentryStore } from '@/stores/useSentryStore';
89

10+
import { useAuthStore } from '../auth';
11+
912
import { useConfig } from './api/useConfig';
1013

1114
export const ConfigProvider = ({ children }: PropsWithChildren) => {
15+
const { userData } = useAuthStore();
1216
const { data: conf } = useConfig();
1317
const { setSentry } = useSentryStore();
1418
const { setTheme } = useCunninghamTheme();
@@ -37,6 +41,23 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
3741
configureCrispSession(conf.CRISP_WEBSITE_ID);
3842
}, [conf?.CRISP_WEBSITE_ID]);
3943

44+
useEffect(() => {
45+
if (!userData?.language || !conf?.LANGUAGES) {
46+
return;
47+
}
48+
49+
conf.LANGUAGES.some(([available_lang]) => {
50+
if (
51+
userData.language === available_lang && // language is expected by user
52+
i18n.language !== available_lang // language not set as expected
53+
) {
54+
void i18n.changeLanguage(available_lang); // change language to expected
55+
return true;
56+
}
57+
return false;
58+
});
59+
}, [conf?.LANGUAGES, userData?.language]);
60+
4061
if (!conf) {
4162
return (
4263
<Box $height="100vh" $width="100vw" $align="center" $justify="center">

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export const BlockNoteEditor = ({
9898
} = useCreateDocAttachment();
9999
const { setHeadings, resetHeadings } = useHeadingStore();
100100
const { i18n } = useTranslation();
101-
const lang = i18n.language;
101+
const lang = i18n.resolvedLanguage;
102102

103103
const uploadFile = useCallback(
104104
async (file: File) => {

0 commit comments

Comments
 (0)