🌐(app-desk) install internationalization

Install internationalization in the Desk app.
We use react-i18next.
This commit is contained in:
Anthony LC
2024-01-19 10:43:09 +01:00
committed by Anthony LC
parent 6a0ed04b0d
commit 01b7ad3f30
12 changed files with 173 additions and 6 deletions

View File

@@ -17,9 +17,11 @@
"dependencies": {
"@openfun/cunningham-react": "2.4.0",
"@tanstack/react-query": "5.18.1",
"i18next": "23.7.16",
"next": "14.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "14.0.0",
"styled-components": "6.1.8",
"zustand": "4.5.0"
},

View File

@@ -5,7 +5,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import './globals.css';
import { useCunninghamTheme } from '@/cunningham';
import '@/i18n/initI18n';
const queryClient = new QueryClient();

View File

@@ -2,6 +2,7 @@
import { Loader } from '@openfun/cunningham-react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import useAuthStore from '@/auth/useAuthStore';
@@ -10,6 +11,7 @@ import { Teams } from './Teams';
export default function Home() {
const { initAuth, authenticated, initialized } = useAuthStore();
const { t } = useTranslation();
useEffect(() => {
if (initialized) {
@@ -26,6 +28,7 @@ export default function Home() {
return (
<main>
<Header />
<h1>{t('Hello Desk !')}</h1>
<Teams />
</main>
);

View File

@@ -6,10 +6,10 @@ declare module '*.svg' {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_KEYCLOAK_URL: string;
NEXT_PUBLIC_KEYCLOAK_REALM: string;
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID: string;
NEXT_PUBLIC_KEYCLOAK_LOGIN: string;
NEXT_PUBLIC_API_URL?: string;
NEXT_PUBLIC_KEYCLOAK_URL?: string;
NEXT_PUBLIC_KEYCLOAK_REALM?: string;
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID?: string;
NEXT_PUBLIC_KEYCLOAK_LOGIN?: string;
}
}

View File

@@ -0,0 +1,55 @@
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

@@ -0,0 +1,3 @@
export const LANGUAGES_ALLOWED = ['en', 'fr'];
export const LANGUAGE_LOCAL_STORAGE = 'people-language';
export const BASE_LANGUAGE = 'fr';

View File

@@ -0,0 +1,29 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { LANGUAGES_ALLOWED, LANGUAGE_LOCAL_STORAGE } from './conf';
import resources from './translations.json';
import { getLanguage } from './utils';
i18n
.use(initReactI18next)
.init({
resources,
lng: getLanguage(),
interpolation: {
escapeValue: false,
},
preload: LANGUAGES_ALLOWED,
})
.catch(() => {
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

@@ -0,0 +1,12 @@
{
"en": {
"translation": {
"Hello Desk !": "Hello Desk !"
}
},
"fr": {
"translation": {
"Hello Desk !": "Bienvenue sur Desk !"
}
}
}

View File

@@ -0,0 +1,26 @@
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 LANGUAGES_ALLOWED.includes(language) ? language : BASE_LANGUAGE;
};

View File

@@ -2,6 +2,8 @@ import { CunninghamProvider } from '@openfun/cunningham-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { PropsWithChildren } from 'react';
import '@/i18n/initI18n';
export const AppWrapper = ({ children }: PropsWithChildren) => {
const queryClient = new QueryClient({
defaultOptions: {

View File

@@ -8,6 +8,12 @@ test.beforeEach(async ({ page }) => {
});
test.describe("Language", () => {
test("checks translation library works", async ({ page }) => {
await expect(
page.locator("h1").first().getByText("Bienvenue sur Desk !"),
).toBeVisible();
});
test("checks the language picker", async ({ page }) => {
const header = page.locator("header").first();