🌐(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();

View File

@@ -268,7 +268,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.22.15", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.9.2":
version "7.23.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==
@@ -4150,6 +4150,13 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
html-tags@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
@@ -4177,6 +4184,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@23.7.16:
version "23.7.16"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.7.16.tgz#7026d18b7a3ac9e2ecfeb78da5e4da5ca33312ef"
integrity sha512-SrqFkMn9W6Wb43ZJ9qrO6U2U4S80RsFMA7VYFSqp7oc7RllQOYDCdRfsse6A7Cq/V8MnpxKvJCYgM8++27n4Fw==
dependencies:
"@babel/runtime" "^7.23.2"
iconv-lite@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -5749,6 +5763,14 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-i18next@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.0.0.tgz#eb39d2245fd1024237828c955f770e409a1ccb12"
integrity sha512-OCrS8rHNAmnr8ggGRDxjakzihrMW7HCbsplduTm3EuuQ6fyvWGT41ksZpqbduYoqJurBmEsEVZ1pILSUWkHZng==
dependencies:
"@babel/runtime" "^7.22.5"
html-parse-stringify "^3.0.1"
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -6574,6 +6596,11 @@ v8-to-istanbul@^9.0.1:
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^2.0.0"
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
w3c-xmlserializer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"