Skip to content

Commit d9bad8c

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 37cc1fe commit d9bad8c

File tree

17 files changed

+225
-143
lines changed

17 files changed

+225
-143
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to
2424
- 🚸(backend) improve users similarity search and sort results #391
2525
- ♻️(frontend) simplify stores #402
2626
- ✨(frontend) update $css Box props type to add styled components RuleSet #423
27+
- ✨(frontend) sync user and frontend language #401
2728

2829
## Fixed
2930

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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import { expect, test } from '@playwright/test';
22

3+
import { createDoc } from './common';
4+
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,80 @@ 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+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
144+
const updatedUserResponse = await response.json();
145+
146+
expect(lang.expectedLocale).toContain(updatedUserResponse.language);
147+
}

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,11 +3,15 @@ import { PropsWithChildren, useEffect } from 'react';
33

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

9+
import { useAuthStore } from '../auth';
10+
811
import { useConfig } from './api/useConfig';
912

1013
export const ConfigProvider = ({ children }: PropsWithChildren) => {
14+
const { userData } = useAuthStore();
1115
const { data: conf } = useConfig();
1216
const { setSentry } = useSentryStore();
1317
const { setTheme } = useCunninghamTheme();
@@ -28,6 +32,23 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
2832
setTheme(conf.FRONTEND_THEME);
2933
}, [conf?.FRONTEND_THEME, setTheme]);
3034

35+
useEffect(() => {
36+
if (!userData?.language || !conf?.LANGUAGES) {
37+
return;
38+
}
39+
40+
conf.LANGUAGES.some(([available_lang]) => {
41+
if (
42+
userData.language === available_lang && // language is expected by user
43+
i18n.language !== available_lang // language not set as expected
44+
) {
45+
void i18n.changeLanguage(available_lang); // change language to expected
46+
return true;
47+
}
48+
return false;
49+
});
50+
}, [conf?.LANGUAGES, userData?.language]);
51+
3152
if (!conf) {
3253
return (
3354
<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
@@ -103,7 +103,7 @@ export const BlockNoteEditor = ({
103103
} = useCreateDocAttachment();
104104
const { setHeadings, resetHeadings } = useHeadingStore();
105105
const { i18n } = useTranslation();
106-
const lang = i18n.language;
106+
const lang = i18n.resolvedLanguage;
107107

108108
const uploadFile = useCallback(
109109
async (file: File) => {

0 commit comments

Comments
 (0)