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 Nov 21, 2024
1 parent f8ff5fd commit 3b3e90c
Show file tree
Hide file tree
Showing 17 changed files with 211 additions and 143 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to
- 🚸(backend) improve users similarity search and sort results #391
- ♻️(frontend) simplify stores #402
- ✨(frontend) update $css Box props type to add styled components RuleSet #423
- ✨(frontend) sync user and frontend language #401

## Fixed

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 @@ -172,7 +172,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
36 changes: 36 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,39 @@ export const mockedAccesses = async (page: Page, json?: object) => {
}
});
};

// 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(),
]);

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const updatedUserResponse = await response.json();

expect(lang.expectedLocale).toContain(updatedUserResponse.language);
}
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
53 changes: 45 additions & 8 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 { TestLanguage, createDoc, waitForLanguageSwitch } 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,44 @@ 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);
});
16 changes: 14 additions & 2 deletions src/frontend/apps/impress/src/core/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect } from 'react';

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

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

/**
* QueryClient:
Expand All @@ -26,6 +27,7 @@ const queryClient = new QueryClient({

export function AppProvider({ children }: { children: React.ReactNode }) {
const { theme } = useCunninghamTheme();
const { userData } = useAuthStore();

const initializeResizeListener = useResponsiveStore(
(state) => state.initializeResizeListener,
Expand All @@ -36,6 +38,16 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
return cleanupResizeListener;
}, [initializeResizeListener]);

useEffect(() => {
if (userData?.language) {
Object.keys(LANGUAGES_ALLOWED).forEach((key) => {
if (userData.language.includes(key) && i18n.language !== key) {
void i18n.changeLanguage(key);
}
});
}
}, [userData?.language]);

return (
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const BlockNoteEditor = ({
} = useCreateDocAttachment();
const { setHeadings, resetHeadings } = useHeadingStore();
const { i18n } = useTranslation();
const lang = i18n.language;
const lang = i18n.resolvedLanguage;

const uploadFile = useCallback(
async (file: File) => {
Expand Down
Loading

0 comments on commit 3b3e90c

Please sign in to comment.