🌐(app-desk) install internationalization
Install internationalization in the Desk app. We use react-i18next.
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
10
src/frontend/apps/desk/src/custom-next.d.ts
vendored
10
src/frontend/apps/desk/src/custom-next.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
55
src/frontend/apps/desk/src/i18n/__tests__/utils.spec.ts
Normal file
55
src/frontend/apps/desk/src/i18n/__tests__/utils.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
3
src/frontend/apps/desk/src/i18n/conf.ts
Normal file
3
src/frontend/apps/desk/src/i18n/conf.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const LANGUAGES_ALLOWED = ['en', 'fr'];
|
||||
export const LANGUAGE_LOCAL_STORAGE = 'people-language';
|
||||
export const BASE_LANGUAGE = 'fr';
|
||||
29
src/frontend/apps/desk/src/i18n/initI18n.ts
Normal file
29
src/frontend/apps/desk/src/i18n/initI18n.ts
Normal 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;
|
||||
12
src/frontend/apps/desk/src/i18n/translations.json
Normal file
12
src/frontend/apps/desk/src/i18n/translations.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"en": {
|
||||
"translation": {
|
||||
"Hello Desk !": "Hello Desk !"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"translation": {
|
||||
"Hello Desk !": "Bienvenue sur Desk !"
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/frontend/apps/desk/src/i18n/utils.ts
Normal file
26
src/frontend/apps/desk/src/i18n/utils.ts
Normal 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;
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user