From c41a1d5261eebb4899c04e43feed760463226eb8 Mon Sep 17 00:00:00 2001 From: Fabien Date: Thu, 6 Feb 2025 16:48:23 +0100 Subject: [PATCH] use typed translation on react i18next on each context Fix #11111,#11002 --- .../i18n/domain/ReactI18nModuleFactory.java | 40 ++++++++++------- .../generator/client/common/i18n/i18n.spec.ts | 6 +-- .../generator/client/common/i18n/i18n.ts | 2 +- src/test/features/client/react-i18n.feature | 7 ++- .../domain/ReactI18nModuleFactoryTest.java | 19 ++++---- ...dBuildI18nModule.HomePage.tsx.approved.txt | 44 +++++++++++++++++++ 6 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 src/test/resources/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.shouldBuildI18nModule.HomePage.tsx.approved.txt diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java index e9c191dc119..a5badc7e0a9 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java @@ -1,6 +1,5 @@ package tech.jhipster.lite.generator.client.react.i18n.domain; -import static tech.jhipster.lite.generator.typescript.common.domain.VitestShortcuts.vitestCoverageExclusion; import static tech.jhipster.lite.module.domain.JHipsterModule.LINE_BREAK; import static tech.jhipster.lite.module.domain.JHipsterModule.append; import static tech.jhipster.lite.module.domain.JHipsterModule.from; @@ -9,6 +8,7 @@ import static tech.jhipster.lite.module.domain.JHipsterModule.moduleBuilder; import static tech.jhipster.lite.module.domain.JHipsterModule.packageName; import static tech.jhipster.lite.module.domain.JHipsterModule.path; +import static tech.jhipster.lite.module.domain.JHipsterModule.text; import static tech.jhipster.lite.module.domain.JHipsterModule.to; import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.COMMON; import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.REACT; @@ -20,12 +20,12 @@ public class ReactI18nModuleFactory { - private static final JHipsterSource APP_SOURCE = from("client/react/i18n/src/main/webapp/app"); - private static final JHipsterSource ASSETS_FR_SOURCE = from("client/react/i18n/src/main/webapp/assets/locales/fr"); - private static final JHipsterSource ASSETS_EN_SOURCE = from("client/react/i18n/src/main/webapp/assets/locales/en"); + private static final JHipsterSource APP_SOURCE = from("client/common/i18n"); + private static final JHipsterSource HOME_CONTEXT_SOURCE = from("client/common/i18n/app"); + private static final JHipsterSource ASSETS_SOURCE = from("client/common/i18n/app/locales"); - private static final String INDEX = "src/main/webapp/"; - private static final String INDEX_TEST = "src/test/webapp/unit/home/infrastructure/primary/"; + private static final String INDEX = "src/main/webapp/app/"; + private static final String INDEX_TEST = "src/test/"; public JHipsterModule buildModule(JHipsterModuleProperties properties) { Assert.notNull("properties", properties); @@ -39,27 +39,36 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { .addDependency(packageName("react-i18next"), REACT) .and() .files() - .batch(APP_SOURCE, to(INDEX + "/app")) + .batch(APP_SOURCE, to(INDEX)) .addFile("i18n.ts") + .addFile("Translations.ts") .and() - .batch(ASSETS_EN_SOURCE, to(INDEX + "assets/locales/en/")) - .addFile("translation.json") + .batch(HOME_CONTEXT_SOURCE, to(INDEX + "home/")) + .addFile("HomeTranslations.ts") .and() - .batch(ASSETS_FR_SOURCE, to(INDEX + "assets/locales/fr/")) - .addFile("translation.json") + .batch(ASSETS_SOURCE, to(INDEX + "home/locales/")) + .addFile("en.ts") + .addFile("fr.ts") + .and() + .batch(APP_SOURCE, to(INDEX_TEST + "webapp/unit")) + .addFile("i18n.spec.ts") .and() .and() .mandatoryReplacements() - .in(path(INDEX + "app/home/infrastructure/primary/HomePage.tsx")) + .in(path(INDEX + "i18n.ts")) + .add(lineAfterText("import LanguageDetector from 'i18next-browser-languagedetector';"), "import { initReactI18next } from 'react-i18next';") + .add(text(".use(LanguageDetector)"), ".use(initReactI18next).use(LanguageDetector)") + .and() + .in(path(INDEX + "home/infrastructure/primary/HomePage.tsx")) .add(lineAfterText("import ReactLogo from '@assets/ReactLogo.png';"), "import { useTranslation } from 'react-i18next';") .add(lineBeforeText("return ("), properties.indentation().times(1) + "const { t } = useTranslation();" + LINE_BREAK) .add(lineAfterText(""), LINE_BREAK + - properties.indentation().times(4) + "

{t('translationEnabled')}

") + properties.indentation().times(4) + "

{t('home.translationEnabled')}

") .and() - .in(path(INDEX + "app/index.tsx")) + .in(path(INDEX + "index.tsx")) .add(lineAfterText("import './index.css';"), "import './i18n';" + LINE_BREAK) .and() - .in(path(INDEX_TEST + "HomePage.spec.tsx")) + .in(path(INDEX_TEST + "webapp/unit/home/infrastructure/primary/HomePage.spec.tsx")) .add(append(), LINE_BREAK + """ describe('Home I18next', () => { it('renders with translation', () => { @@ -77,7 +86,6 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { });""" ) .and() .and() - .apply(vitestCoverageExclusion("src/main/webapp/app/i18n.ts")) .build(); //@formatter:off } diff --git a/src/main/resources/generator/client/common/i18n/i18n.spec.ts b/src/main/resources/generator/client/common/i18n/i18n.spec.ts index f837218e0dc..9283732a17a 100644 --- a/src/main/resources/generator/client/common/i18n/i18n.spec.ts +++ b/src/main/resources/generator/client/common/i18n/i18n.spec.ts @@ -1,13 +1,13 @@ import i18n from '@/i18n'; -import { mergeTranslations } from '@/Translations'; +import { mergeTranslations, type Translations } from '@/Translations'; describe('i18n configuration', () => { it('loads en translation', () => { - expect(i18n.getResourceBundle('en', '')['home']['translationEnabled']).toBe('Internationalization enabled'); + expect((i18n.getResourceBundle('fr', '') as Translations).home.translationEnabled).toBe('Internationalisation activée'); }); it('loads fr translation', () => { - expect(i18n.getResourceBundle('fr', '')['home']['translationEnabled']).toBe('Internationalisation activée'); + expect((i18n.getResourceBundle('fr', '') as Translations).home.translationEnabled).toBe('Internationalisation activée'); }); describe('mergeTranslations function', () => { diff --git a/src/main/resources/generator/client/common/i18n/i18n.ts b/src/main/resources/generator/client/common/i18n/i18n.ts index a84026e1aee..1aced7935e8 100644 --- a/src/main/resources/generator/client/common/i18n/i18n.ts +++ b/src/main/resources/generator/client/common/i18n/i18n.ts @@ -3,7 +3,7 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import { homeTranslations } from './home/HomeTranslations'; import { toTranslationResources } from './Translations'; -i18n.use(LanguageDetector).init({ +void i18n.use(LanguageDetector).init({ fallbackLng: 'en', debug: false, interpolation: { diff --git a/src/test/features/client/react-i18n.feature b/src/test/features/client/react-i18n.feature index d97724c1748..71b07a9736e 100644 --- a/src/test/features/client/react-i18n.feature +++ b/src/test/features/client/react-i18n.feature @@ -8,7 +8,6 @@ Feature: React i18n | react-i18next | Then I should have files in "src/main/webapp/app" | i18n.ts | - And I should have files in "src/main/webapp/assets/locales/en" - | translation.json | - And I should have files in "src/main/webapp/assets/locales/fr" - | translation.json | + And I should have files in "src/main/webapp/app/home/locales" + | en.ts | + | fr.ts | diff --git a/src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java index f1c2ea89d97..f04be350433 100644 --- a/src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java +++ b/src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java @@ -30,21 +30,22 @@ void shouldBuildI18nModule() { .containing(nodeDependency("i18next-http-backend")) .containing(nodeDependency("react-i18next")) .and() - .hasFiles("src/main/webapp/app/i18n.ts") + .hasFiles("src/test/webapp/unit/i18n.spec.ts", "src/main/webapp/app/Translations.ts") + .hasFile("src/main/webapp/app/i18n.ts") + .containing("import { initReactI18next } from 'react-i18next';") + .containing("use(initReactI18next)") + .and() .hasFile("src/main/webapp/app/index.tsx") .containing("import './i18n'") .and() .hasFile(HOME_PAGE_TSX) - .containing("import { useTranslation } from 'react-i18next") - .containing("const { t } = useTranslation();") - .containing("{t('translationEnabled')}") + .matchingSavedSnapshot() .and() - .hasPrefixedFiles("src/main/webapp/assets/locales/", "en/translation.json", "fr/translation.json") - .hasFile("src/test/webapp/unit/home/infrastructure/primary/HomePage.spec.tsx") - .containing("describe('Home I18next', () => {") + .hasFile("src/main/webapp/app/home/HomeTranslations.ts") .and() - .hasFile("vitest.config.ts") - .containing("'src/main/webapp/app/i18n.ts',"); + .hasPrefixedFiles("src/main/webapp/app/home/locales", "en.ts", "fr.ts") + .hasFile("src/test/webapp/unit/home/infrastructure/primary/HomePage.spec.tsx") + .containing("describe('Home I18next', () => {"); } private ModuleFile app() { diff --git a/src/test/resources/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.shouldBuildI18nModule.HomePage.tsx.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.shouldBuildI18nModule.HomePage.tsx.approved.txt new file mode 100644 index 00000000000..4f532fd97c6 --- /dev/null +++ b/src/test/resources/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.shouldBuildI18nModule.HomePage.tsx.approved.txt @@ -0,0 +1,44 @@ +import './HomePage.css'; + +import JHipsterLiteNeonBlue from '@assets/JHipster-Lite-neon-blue.png'; +import ReactLogo from '@assets/ReactLogo.png'; +import { useTranslation } from 'react-i18next'; + +function HomePage() { + const { t } = useTranslation(); + + return ( +
+
+ React logo +
+ JHipster logo +

React + TypeScript + Vite

+ +

{t('home.translationEnabled')}

+

+ Recommended IDE setup:  + + VSCode + +

+ +

+ + Vite Documentation + +  |  + + React Documentation + +

+ +

+ Edit src/main/webapp/app/common/primary/app/App.tsx to test hot module replacement. +

+
+
+ ); +} + +export default HomePage;