Skip to content

Commit

Permalink
✨(frontend) sync user and frontend language
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rvveber committed Dec 4, 2024
1 parent 5ab181c commit 6a562b4
Show file tree
Hide file tree
Showing 16 changed files with 239 additions and 150 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to

## Changed

- ✨(frontend) sync user and frontend language #401

## Fixed

- 🐛(backend) invitation e-mails in receivers language #401
Expand Down
2 changes: 1 addition & 1 deletion src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class UserSerializer(serializers.ModelSerializer):

class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name"]
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]


Expand Down
17 changes: 6 additions & 11 deletions src/backend/demo/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,12 @@
}

DEV_USERS = [
{"username": "impress", "email": "[email protected]", "language": "en-us"},
{"username": "user-e2e-webkit", "email": "[email protected]", "language": "en-us"},
{"username": "user-e2e-firefox", "email": "[email protected]", "language": "en-us"},
{
"username": "impress",
"email": "[email protected]",
"username": "user-e2e-chromium",
"email": "[email protected]",
"language": "en-us",
},
{
"username": "user-e2e-webkit",
"email": "[email protected]",
},
{
"username": "user-e2e-firefox",
"email": "[email protected]",
},
{"username": "user-e2e-chromium", "email": "[email protected]"},
]
3 changes: 2 additions & 1 deletion src/backend/demo/management/commands/create_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ def create_demo(stdout):
is_superuser=False,
is_active=True,
is_staff=False,
language=random.choice(settings.LANGUAGES)[0],
language=dev_user["language"]
or random.choice(settings.LANGUAGES)[0],
)
)

Expand Down
35 changes: 0 additions & 35 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,6 @@ test.beforeEach(async ({ page }) => {
});

test.describe('Doc Editor', () => {
test('it check translations of the slash menu when changing language', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-toolbar', browserName, 1);

const header = page.locator('header').first();
const editor = page.locator('.ProseMirror');
// Trigger slash menu to show english menu
await editor.click();
await editor.fill('/');
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
await header.click();
await expect(page.getByText('Headings', { exact: true })).toBeHidden();

// Reset menu
await editor.click();
await editor.fill('');

// Change language to French
await header.click();
await header.getByRole('combobox').getByText('English').click();
await header.getByRole('option', { name: 'Français' }).click();
await expect(
header.getByRole('combobox').getByText('Français'),
).toBeVisible();

// Trigger slash menu to show french menu
await editor.click();
await editor.fill('/');
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
await header.click();
await expect(page.getByText('Titres', { exact: true })).toBeHidden();
});

test('it checks default toolbar buttons are displayed', async ({
page,
browserName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,13 @@ test.describe('Document create member', () => {
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
const responseCreateInvitation = await responsePromiseCreateInvitation;
expect(responseCreateInvitation.ok()).toBeTruthy();
expect(
responseCreateInvitation.request().headers()['content-language'],
).toBe('en-us');

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

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

test('The invitation endpoint get the language of the website', async ({
page,
browserName,
}) => {
await createDoc(page, 'user-invitation', browserName, 1);

const header = page.locator('header').first();
await header.getByRole('combobox').getByText('EN').click();
await header.getByRole('option', { name: 'FR' }).click();

await page.getByRole('button', { name: 'Partager' }).click();

const inputSearch = page.getByLabel(
/Trouver un membre à ajouter au document/,
);

const email = randomName('[email protected]', browserName, 1)[0];
await inputSearch.fill(email);
await page.getByRole('option', { name: email }).click();

// Choose a role
await page.getByRole('combobox', { name: /Choisissez un rôle/ }).click();
await page.getByRole('option', { name: 'Administrateur' }).click();

const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
response.url().includes('/invitations/') && response.status() === 201,
);

await page.getByRole('button', { name: 'Valider' }).click();

// Check invitation sent
await expect(page.getByText(`Invitation envoyée à ${email}`)).toBeVisible();
const responseCreateInvitation = await responsePromiseCreateInvitation;
expect(responseCreateInvitation.ok()).toBeTruthy();
expect(
responseCreateInvitation.request().headers()['content-language'],
).toBe('fr-fr');
});

test('it manages invitation', async ({ page, browserName }) => {
await createDoc(page, 'user-invitation', browserName, 1);

Expand Down
90 changes: 81 additions & 9 deletions src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { expect, test } from '@playwright/test';
import { Page, expect, test } from '@playwright/test';

import { createDoc } from './common';

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

test.describe('Language', () => {
test('checks the language picker', async ({ page }) => {
test('checks language switching', async ({ page }) => {
const header = page.locator('header').first();

// initial language should be english
await expect(
page.getByRole('button', {
name: 'Create a new document',
}),
).toBeVisible();

const header = page.locator('header').first();
await header.getByRole('combobox').getByText('English').click();
await header.getByRole('option', { name: 'Français' }).click();
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);

await expect(
header.getByRole('combobox').getByText('Français'),
).toBeVisible();
Expand Down Expand Up @@ -63,12 +68,79 @@ test.describe('Language', () => {
// Check for English 404 response
await check404Response('Not found.');

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

// Check for French 404 response
await check404Response('Pas trouvé.');
});

test('it check translations of the slash menu when changing language', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-toolbar', browserName, 1);

const header = page.locator('header').first();
const editor = page.locator('.ProseMirror');
// Trigger slash menu to show english menu
await editor.click();
await editor.fill('/');
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
await header.click();
await expect(page.getByText('Headings', { exact: true })).toBeHidden();

// Reset menu
await editor.click();
await editor.fill('');

// Change language to French
await waitForLanguageSwitch(page, TestLanguage.French);

// Trigger slash menu to show french menu
await editor.click();
await editor.fill('/');
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
await header.click();
await expect(page.getByText('Titres', { exact: true })).toBeHidden();
});
});

test.afterEach(async ({ page }) => {
// Switch back to English - important for other tests to run as expected
await waitForLanguageSwitch(page, TestLanguage.English);
});

// language helper
export const TestLanguage = {
English: {
label: 'English',
expectedLocale: ['en-us'],
},
French: {
label: 'Français',
expectedLocale: ['fr-fr'],
},
} as const;

type TestLanguageKey = keyof typeof TestLanguage;
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];

export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
const header = page.locator('header').first();
await header.getByRole('combobox').click();

const [response] = await Promise.all([
page.waitForResponse(
(resp) =>
resp.url().includes('/user') && resp.request().method() === 'PATCH',
),
header.getByRole('option', { name: lang.label }).click(),
]);

const updatedUserResponse = await response.json();

expect(lang.expectedLocale).toContain(updatedUserResponse.language);
}
1 change: 0 additions & 1 deletion src/frontend/apps/impress/src/core/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect } from 'react';

import { useCunninghamTheme } from '@/cunningham';
import '@/i18n/initI18n';
import { useResponsiveStore } from '@/stores/';

import { Auth } from './auth/';
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/apps/impress/src/core/auth/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
* @property {string} id - The id of the user.
* @property {string} email - The email of the user.
* @property {string} name - The name of the user.
* @property {string} language - The language of the user.
*/
export interface User {
id: string;
email: string;
full_name: string;
short_name: string;
language: string;
}
21 changes: 21 additions & 0 deletions src/frontend/apps/impress/src/core/config/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { PropsWithChildren, useEffect } from 'react';

import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import i18n from '@/i18n/initI18n';
import { configureCrispSession } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';

import { useAuthStore } from '../auth';

import { useConfig } from './api/useConfig';

export const ConfigProvider = ({ children }: PropsWithChildren) => {
const { userData } = useAuthStore();
const { data: conf } = useConfig();
const { setSentry } = useSentryStore();
const { setTheme } = useCunninghamTheme();
Expand Down Expand Up @@ -37,6 +41,23 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
configureCrispSession(conf.CRISP_WEBSITE_ID);
}, [conf?.CRISP_WEBSITE_ID]);

useEffect(() => {
if (!userData?.language || !conf?.LANGUAGES) {
return;
}

conf.LANGUAGES.some(([available_lang]) => {
if (
userData.language === available_lang && // language is expected by user
i18n.language !== available_lang // language not set as expected
) {
void i18n.changeLanguage(available_lang); // change language to expected
return true;
}
return false;
});
}, [conf?.LANGUAGES, userData?.language]);

if (!conf) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const BlockNoteEditor = ({
useSaveDoc(doc.id, provider.document, !readOnly);
const { setHeadings, resetHeadings } = useHeadingStore();
const { i18n } = useTranslation();
const lang = i18n.language;
const lang = i18n.resolvedLanguage;

const { uploadFile, errorAttachment } = useUploadFile(doc.id);

Expand Down
Loading

0 comments on commit 6a562b4

Please sign in to comment.