🌐(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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user