Skip to content

Commit 1499466

Browse files
QRuhierMailineN
andauthored
refactor: migrate to i18next (#312)
* chore: migrate to i18next: * bump: 2.4.1-i18next-migration.0 * bump: 2.4.1-technical-debt.0 * bump: 2.4.2-technical-debt.0 * bump: 2.4.2-rc-technical-debt.0 * fix: welcome modal * bump: 2.4.3-rc-technical-debt.1 * fix: redirect button * fix: translation (#314) * chore: fix translation autocompletion * fix: validation modal content * fix: header title using metadata * fix: welcome page document title * fix: accessibility page all english translation * fix: error page english translation * fix: exit modal translation * fix: VTLDevTools translation * type: fix type issue on translation keys * fix: visualize page translation & move translation file * bump: 2.4.3-rc-technical-debt.3 --------- Co-authored-by: Mailine Nguyen <64129348+MailineN@users.noreply.github.com>
1 parent 85bb649 commit 1499466

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2257
-3444
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Switched i18n library from i18nifty to i18next.
13+
1014
## [2.4.2](https://github.com/InseeFr/stromae-dsfr/releases/tag/2.4.2) - 2026-02-09
1115

1216
### Changed

__mocks__/i18nifty.js

Lines changed: 0 additions & 26 deletions
This file was deleted.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,15 @@
3737
"date-fns": "^4.1.0",
3838
"fast-deep-equal": "^3.1.3",
3939
"he": "^1.2.0",
40+
"i18next": "^25.8.0",
41+
"i18next-browser-languagedetector": "^8.2.0",
4042
"i18nifty": "^3.2.6",
4143
"oidc-spa": "10.0.8",
4244
"react": "^19.2.3",
4345
"react-dom": "^19.2.3",
4446
"react-hook-form": "^7.66.0",
4547
"react-hot-toast": "^2.6.0",
48+
"react-i18next": "^16.5.3",
4649
"react-number-format": "^5.4.4",
4750
"tsafe": "^1.8.12",
4851
"tss-react": "^4.9.19",

pnpm-lock.yaml

Lines changed: 863 additions & 1101 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { TelemetryProvider } from '@/contexts/TelemetryContext'
1212
import { routeTree } from '@/router/router'
1313

14+
import './libs/i18n'
1415
import { BASE_PATH } from './utils/env'
1516

1617
startReactDsfr({

src/components/error/ErrorComponent.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,34 @@ import ArtWorkBackground from '@codegouvfr/react-dsfr/dsfr/artwork/background/ov
44
import TechnicalError from '@codegouvfr/react-dsfr/dsfr/artwork/pictograms/system/technical-error.svg'
55
import ArtWork from '@codegouvfr/react-dsfr/dsfr/artwork/system.svg'
66
import { useNavigate } from '@tanstack/react-router'
7+
import { useTranslation } from 'react-i18next'
78

89
import { Container } from '@/components/Container'
910
import { errorNormalizer } from '@/components/error/errorNormalizer'
1011
import { useDocumentTitle } from '@/hooks/useDocumentTitle'
11-
import { declareComponentKeys, useTranslation } from '@/i18n'
1212

1313
type Props = {
1414
error: unknown
1515
reset?: () => void
1616
redirectTo: 'home' | 'portal' | 'visualizeForm' | undefined
1717
}
1818

19+
const REDIRECT_KEY_MAP = {
20+
home: 'error.redirectHome',
21+
portal: 'error.redirectPortal',
22+
visualizeForm: 'error.redirectVisualizeForm',
23+
} as const satisfies Record<NonNullable<Props['redirectTo']>, string>
24+
1925
export function ErrorComponent(props: Props) {
2026
const { error, redirectTo, reset } = props
2127
const navigate = useNavigate()
22-
const { t } = useTranslation({ ErrorComponent })
28+
const { t } = useTranslation()
2329
const { title, subtitle, paragraph, code } = errorNormalizer(error)
2430

2531
useDocumentTitle(title)
2632

33+
const redirectKey = redirectTo ? REDIRECT_KEY_MAP[redirectTo] : ''
34+
2735
return (
2836
<Container>
2937
<div
@@ -60,7 +68,7 @@ export function ErrorComponent(props: Props) {
6068
}
6169
})()}
6270
>
63-
{t('error button redirect to', { redirectTo })}
71+
{redirectKey ? t(redirectKey) : ''}
6472
</Button>
6573
)}
6674
</div>
@@ -109,12 +117,3 @@ export function ErrorComponent(props: Props) {
109117
</Container>
110118
)
111119
}
112-
113-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
114-
const { i18n } = declareComponentKeys<{
115-
K: 'error button redirect to'
116-
P: { redirectTo: Props['redirectTo'] }
117-
R: string
118-
}>()({ ErrorComponent })
119-
120-
export type I18n = typeof i18n
Lines changed: 33 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import type { ReactNode } from 'react'
22

33
import { AxiosError } from 'axios'
4+
import i18n from 'i18next'
45

5-
import { declareComponentKeys, getTranslation } from '@/i18n'
6-
7-
import { type ZodErrorName, ZodErrorWithName } from './ZodErrorWithName'
6+
import { ZodErrorWithName } from './ZodErrorWithName'
87
import { NotFoundError } from './notFoundError'
98

109
type ErrorNormalized = {
@@ -14,77 +13,77 @@ type ErrorNormalized = {
1413
code?: number
1514
}
1615

17-
const { t } = getTranslation('errorNormalizer')
18-
1916
export function errorNormalizer(error: unknown): ErrorNormalized {
17+
const t = i18n.t
18+
2019
if (error instanceof NotFoundError) {
2120
return {
22-
title: t('notFound.title'),
23-
subtitle: t('notFound.subtitle'),
24-
paragraph: t('notFound.paragraph'),
21+
title: t('error.notFound.title'),
22+
subtitle: t('error.notFound.subtitle'),
23+
paragraph: t('error.notFound.paragraph'),
2524
code: 404,
2625
}
2726
}
2827

2928
if (error instanceof AxiosError) {
3029
if (!error.response) {
3130
return {
32-
title: t('connectionError.title'),
33-
subtitle: t('connectionError.subtitle'),
34-
paragraph: t('connectionError.paragraph'),
31+
title: t('error.connectionError.title'),
32+
subtitle: t('error.connectionError.subtitle'),
33+
paragraph: t('error.connectionError.paragraph'),
3534
}
3635
}
3736
const status = error.response.status
3837
switch (status) {
3938
case 404:
4039
return {
41-
title: t('resourceNotFound.title'),
42-
subtitle: t('resourceNotFound.subtitle'),
43-
paragraph: t('resourceNotFound.paragraph'),
40+
title: t('error.resourceNotFound.title'),
41+
subtitle: t('error.resourceNotFound.subtitle'),
42+
paragraph: t('error.resourceNotFound.paragraph'),
4443
code: status,
4544
}
4645
case 401:
4746
return {
48-
title: t('unauthorized.title'),
49-
subtitle: t('unauthorized.subtitle'),
50-
paragraph: t('unauthorized.paragraph'),
47+
title: t('error.unauthorized.title'),
48+
subtitle: t('error.unauthorized.subtitle'),
49+
paragraph: t('error.unauthorized.paragraph'),
5150
code: status,
5251
}
5352
case 403:
5453
return {
55-
title: t('forbidden.title'),
56-
subtitle: t('forbidden.subtitle'),
57-
paragraph: t('forbidden.paragraph'),
54+
title: t('error.forbidden.title'),
55+
subtitle: t('error.forbidden.subtitle'),
56+
paragraph: t('error.forbidden.paragraph'),
5857
code: status,
5958
}
6059
case 400:
6160
return {
62-
title: t('badRequest.title'),
63-
subtitle: t('badRequest.subtitle'),
64-
paragraph: t('badRequest.paragraph'),
61+
title: t('error.badRequest.title'),
62+
subtitle: t('error.badRequest.subtitle'),
63+
paragraph: t('error.badRequest.paragraph'),
6564
code: status,
6665
}
6766
case 500:
6867
return {
69-
title: t('serverError.title'),
70-
subtitle: t('serverError.subtitle'),
71-
paragraph: t('serverError.paragraph'),
68+
title: t('error.serverError.title'),
69+
subtitle: t('error.serverError.subtitle'),
70+
paragraph: t('error.serverError.paragraph'),
7271
code: status,
7372
}
7473
default:
7574
return {
76-
title: t('unhandledError.title'),
77-
subtitle: t('unhandledError.subtitle'),
78-
paragraph: t('unhandledError.paragraph'),
75+
title: t('error.unhandledError.title'),
76+
subtitle: t('error.unhandledError.subtitle'),
77+
paragraph: t('error.unhandledError.paragraph'),
7978
code: status,
8079
}
8180
}
8281
}
8382

8483
if (error instanceof ZodErrorWithName) {
8584
return {
86-
title: t('validationError.title'),
87-
subtitle: t('validationError.subtitle', { name: error.name }),
85+
title: t('error.validationError.title'),
86+
subtitle: t('error.validationError.subtitle', { name: error.name }),
8887
paragraph: (
8988
<ul>
9089
{error.errors.map((e, index) => (
@@ -97,32 +96,8 @@ export function errorNormalizer(error: unknown): ErrorNormalized {
9796
}
9897
}
9998
return {
100-
title: t('unknownError.title'),
101-
subtitle: t('unknownError.subtitle'),
102-
paragraph: t('unknownError.paragraph'),
99+
title: t('error.unknownError.title'),
100+
subtitle: t('error.unknownError.subtitle'),
101+
paragraph: t('error.unknownError.paragraph'),
103102
}
104103
}
105-
106-
type GenerateKeys<BaseKey extends string> =
107-
`${BaseKey}.${Exclude<keyof ErrorNormalized, 'code'>}`
108-
109-
type ValidationError =
110-
| 'validationError.title'
111-
| { K: 'validationError.subtitle'; P: { name: ZodErrorName }; R: string }
112-
113-
type AllErrorKeys =
114-
| GenerateKeys<'notFound'>
115-
| GenerateKeys<'connectionError'>
116-
| GenerateKeys<'resourceNotFound'>
117-
| GenerateKeys<'unauthorized'>
118-
| GenerateKeys<'forbidden'>
119-
| GenerateKeys<'badRequest'>
120-
| GenerateKeys<'serverError'>
121-
| GenerateKeys<'unhandledError'>
122-
| GenerateKeys<'unknownError'>
123-
| ValidationError
124-
125-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
126-
const { i18n } = declareComponentKeys<AllErrorKeys>()('errorNormalizer')
127-
128-
export type I18n = typeof i18n

src/components/layout/AutoLogoutCountdown.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useEffect } from 'react'
22

3-
import { declareComponentKeys } from 'i18nifty'
3+
import { useTranslation } from 'react-i18next'
44

55
import { executePreLogoutActions } from '@/hooks/prelogout'
6-
import { useTranslation } from '@/i18n'
76
import { useOidc } from '@/oidc'
87

98
export function AutoLogoutCountdown() {
10-
const { t } = useTranslation({ AutoLogoutCountdown })
9+
const { t } = useTranslation()
1110
const { autoLogoutState } = useOidc()
1211

1312
useEffect(() => {
@@ -42,20 +41,9 @@ export function AutoLogoutCountdown() {
4241
}}
4342
>
4443
<div style={{ textAlign: 'center' }}>
45-
<p>{t('paragraph still there')}</p>
46-
<p>{t('paragraph logged out in', { secondsLeft })}</p>
44+
<p>{t('header.stillThere')}</p>
45+
<p>{t('header.loggedOutIn', { secondsLeft })}</p>
4746
</div>
4847
</div>
4948
)
5049
}
51-
52-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
53-
const { i18n } = declareComponentKeys<
54-
| 'paragraph still there'
55-
| {
56-
K: 'paragraph logged out in'
57-
P: { secondsLeft: number }
58-
R: string
59-
}
60-
>()({ AutoLogoutCountdown })
61-
export type I18n = typeof i18n

0 commit comments

Comments
 (0)