🐛(i18n) same frontend and backend language using shared cookies

frontend: switch to cookie-based language selection
backend: use cookie for language
This commit is contained in:
rvveber
2024-10-22 18:17:53 +02:00
committed by Anthony LC
parent ff364f8b3d
commit 3c374e3cc7
9 changed files with 58 additions and 96 deletions

View File

@@ -18,6 +18,7 @@ and this project adheres to
## Fixed
- 🐛(backend) require right to manage document accesses to see invitations #369
- 🐛(i18n) same frontend and backend language using shared cookies #365
- 🐛(frontend) add default toolbar buttons #355
@@ -27,7 +28,7 @@ and this project adheres to
- ✨AI to doc editor #250
- ✨(backend) allow uploading more types of attachments #309
- ✨(frontend) add buttons to copy document to clipboard as HTML/Markdown #300
- ✨(frontend) add buttons to copy document to clipboard as HTML/Markdown #318
## Changed

View File

@@ -223,6 +223,7 @@ class Base(Configuration):
# Languages
LANGUAGE_CODE = values.Value("en-us")
LANGUAGE_COOKIE_NAME = "docs_language" # cookie & language is set from frontend
DRF_NESTED_MULTIPART_PARSER = {
# output of parser is converted to querydict

View File

@@ -25,4 +25,38 @@ test.describe('Language', () => {
}),
).toBeVisible();
});
test('checks that backend uses the same language as the frontend', async ({
page,
}) => {
// Helper function to intercept and assert 404 response
const check404Response = async (expectedDetail: string) => {
const expectedBackendResponse = page.waitForResponse(
(response) =>
response.url().includes('/api') &&
response.url().includes('non-existent-doc-uuid') &&
response.status() === 404,
);
// Trigger the specific 404 XHR response by navigating to a non-existent document
await page.goto('/docs/non-existent-doc-uuid');
// Assert that the intercepted error message is in the expected language
const interceptedBackendResponse = await expectedBackendResponse;
expect(await interceptedBackendResponse.json()).toStrictEqual({
detail: expectedDetail,
});
};
// 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();
// Check for French 404 response
await check404Response('Pas trouvé.');
});
});

View File

@@ -23,6 +23,7 @@
"@openfun/cunningham-react": "2.9.4",
"@tanstack/react-query": "5.59.15",
"i18next": "23.16.2",
"i18next-browser-languagedetector": "8.0.0",
"idb": "8.0.0",
"lodash": "4.17.21",
"luxon": "3.5.0",

View File

@@ -1,55 +0,0 @@
import { LANGUAGE_LOCAL_STORAGE } from '../conf';
import { getLanguage, splitLocaleCode } from '../utils';
describe('i18n utils', () => {
afterEach(() => {
localStorage.removeItem(LANGUAGE_LOCAL_STORAGE);
});
it('checks language code is correctly splitted', () => {
expect(splitLocaleCode('fr_FR')).toEqual({ language: 'fr', region: 'FR' });
expect(splitLocaleCode('en_US')).toEqual({ language: 'en', region: 'US' });
expect(splitLocaleCode('en')).toEqual({
language: 'en',
region: undefined,
});
expect(splitLocaleCode('fr-FR')).toEqual({ language: 'fr', region: 'FR' });
expect(splitLocaleCode('en-US')).toEqual({ language: 'en', region: 'US' });
});
it('checks that we get expected language from local storage', () => {
localStorage.setItem(LANGUAGE_LOCAL_STORAGE, 'fr_FR');
expect(getLanguage()).toEqual('fr');
localStorage.removeItem(LANGUAGE_LOCAL_STORAGE);
localStorage.setItem(LANGUAGE_LOCAL_STORAGE, 'en_FR');
expect(getLanguage()).toEqual('en');
localStorage.setItem(LANGUAGE_LOCAL_STORAGE, 'xx_XX');
expect(getLanguage()).toEqual('fr');
});
it('checks that we get expected language from browser', () => {
Object.defineProperty(navigator, 'language', {
value: 'fr',
writable: false,
configurable: true,
});
expect(getLanguage()).toEqual('fr');
Object.defineProperty(navigator, 'language', {
value: 'en',
writable: false,
configurable: true,
});
expect(getLanguage()).toEqual('en');
Object.defineProperty(navigator, 'language', {
value: 'xx',
writable: false,
configurable: true,
});
expect(getLanguage()).toEqual('fr');
});
});

View File

@@ -2,5 +2,5 @@ export const LANGUAGES_ALLOWED: { [key: string]: string } = {
en: 'English',
fr: 'Français',
};
export const LANGUAGE_LOCAL_STORAGE = 'impress-language';
export const BASE_LANGUAGE = 'fr';
export const LANGUAGE_COOKIE_NAME = 'docs_language';
export const BASE_LANGUAGE = 'en';

View File

@@ -1,15 +1,23 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import { LANGUAGES_ALLOWED, LANGUAGE_LOCAL_STORAGE } from './conf';
import { BASE_LANGUAGE, LANGUAGES_ALLOWED, LANGUAGE_COOKIE_NAME } from './conf';
import resources from './translations.json';
import { getLanguage } from './utils';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
lng: getLanguage(),
fallbackLng: BASE_LANGUAGE,
supportedLngs: Object.keys(LANGUAGES_ALLOWED),
detection: {
order: ['cookie', 'navigator'], // detection order
caches: ['cookie'], // Use cookies to store the language preference
lookupCookie: LANGUAGE_COOKIE_NAME,
cookieMinutes: 525600, // Expires after one year
},
interpolation: {
escapeValue: false,
},
@@ -20,11 +28,4 @@ i18n
throw new Error('i18n initialization failed');
});
// Save language in local storage
i18n.on('languageChanged', (lng) => {
if (typeof window !== 'undefined') {
localStorage.setItem(LANGUAGE_LOCAL_STORAGE, lng);
}
});
export default i18n;

View File

@@ -1,28 +0,0 @@
import {
BASE_LANGUAGE,
LANGUAGES_ALLOWED,
LANGUAGE_LOCAL_STORAGE,
} from './conf';
export const splitLocaleCode = (language: string) => {
const locale = language.split(/[-_]/);
return {
language: locale[0],
region: locale.length === 2 ? locale[1] : undefined,
};
};
export const getLanguage = () => {
if (typeof window === 'undefined') {
return BASE_LANGUAGE;
}
const languageStore =
localStorage.getItem(LANGUAGE_LOCAL_STORAGE) || navigator?.language;
const language = splitLocaleCode(languageStore).language;
return Object.keys(LANGUAGES_ALLOWED).includes(language)
? language
: BASE_LANGUAGE;
};

View File

@@ -7167,6 +7167,13 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
i18next-browser-languagedetector@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz#b6fdd9b43af67c47f2c26c9ba27710a1eaf31e2f"
integrity sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==
dependencies:
"@babel/runtime" "^7.23.2"
i18next-parser@9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-9.0.2.tgz#f9d627422d33c352967556c8724975d58f1f5a95"