From aac4bfb40f4eebb63fdd700b7098decc23684997 Mon Sep 17 00:00:00 2001 From: fabienpuissant Date: Thu, 22 Aug 2024 23:09:38 +0200 Subject: [PATCH 1/3] add react internationalization create react internalization module using i18next 23.14.0 with language detector see https://react.i18next.com/latest/using-with-hooks Fix #10413 --- .../ReactI18nApplicationService.java | 20 ++++ .../domain/ReactI18nModuleFactory.java | 70 ++++++++++++ .../primary/ReactI18nModuleConfiguration.java | 27 +++++ .../client/tools/reacti18n/package-info.java | 2 + .../slug/domain/JHLiteFeatureSlug.java | 1 + .../slug/domain/JHLiteModuleSlug.java | 1 + .../lite/module/domain/JHipsterModule.java | 4 + .../src/main/webapp/app/i18n.ts.mustache | 24 ++++ .../main/webapp/assets/english.json.mustache | 3 + .../main/webapp/assets/french.json.mustache | 3 + .../generator/dependencies/react/package.json | 6 +- src/test/features/client/reacti18n.feature | 13 +++ .../domain/ReactI18nModuleFactoryTest.java | 104 ++++++++++++++++++ .../resources/projects/react-app/App.spec.tsx | 12 ++ 14 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java create mode 100644 src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java create mode 100644 src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java create mode 100644 src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/package-info.java create mode 100644 src/main/resources/generator/client/common/reacti18n/src/main/webapp/app/i18n.ts.mustache create mode 100644 src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/english.json.mustache create mode 100644 src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/french.json.mustache create mode 100644 src/test/features/client/reacti18n.feature create mode 100644 src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java create mode 100644 src/test/resources/projects/react-app/App.spec.tsx diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java new file mode 100644 index 00000000000..e9417c5e1d1 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java @@ -0,0 +1,20 @@ +package tech.jhipster.lite.generator.client.tools.reacti18n.application; + +import org.springframework.stereotype.Service; +import tech.jhipster.lite.generator.client.tools.reacti18n.domain.ReactI18nModuleFactory; +import tech.jhipster.lite.module.domain.JHipsterModule; +import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties; + +@Service +public class ReactI18nApplicationService { + + private final ReactI18nModuleFactory factory; + + public ReactI18nApplicationService() { + factory = new ReactI18nModuleFactory(); + } + + public JHipsterModule buildModule(JHipsterModuleProperties properties) { + return factory.buildModule(properties); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java new file mode 100644 index 00000000000..a1e6ed835b2 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java @@ -0,0 +1,70 @@ +package tech.jhipster.lite.generator.client.tools.reacti18n.domain; + +import static tech.jhipster.lite.module.domain.JHipsterModule.*; + +import tech.jhipster.lite.module.domain.JHipsterModule; +import tech.jhipster.lite.module.domain.file.JHipsterSource; +import tech.jhipster.lite.module.domain.packagejson.VersionSource; +import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties; +import tech.jhipster.lite.shared.error.domain.Assert; + +public class ReactI18nModuleFactory { + + private static final JHipsterSource APP_SOURCE = from("client/common/reacti18n/src/main/webapp/app"); + private static final JHipsterSource ASSETS_SOURCE = from("client/common/reacti18n/src/main/webapp/assets"); + + private static final String INDEX = "src/main/webapp/"; + private static final String INDEX_TEST = "src/test/webapp/unit/common/primary/app/"; + + public JHipsterModule buildModule(JHipsterModuleProperties properties) { + Assert.notNull("properties", properties); + + //@formatter:off + return moduleBuilder(properties) + .packageJson() + .addDependency(packageName("i18next"), VersionSource.REACT) + .addDependency(packageName("i18next-browser-languagedetector"), VersionSource.REACT) + .addDependency(packageName("i18next-http-backend"), VersionSource.REACT) + .addDependency(packageName("react-i18next"), VersionSource.REACT) + .and() + .files() + .add(APP_SOURCE.template("i18n.ts"), to(INDEX + "app/i18n.ts")) + .add(ASSETS_SOURCE.template("english.json"), to(INDEX + "assets/locales/en/translation.json")) + .add(ASSETS_SOURCE.template("french.json"), to(INDEX + "assets/locales/fr/translation.json")) + .and() + .mandatoryReplacements() + .in(path(INDEX + "app/common/primary/app/App.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')}

") + .and() + .in(path(INDEX + "app/index.tsx")) + .add(lineAfterText("import './index.css';"), "import './i18n';" + LINE_BREAK) + .and() + .in(path(INDEX_TEST + "App.spec.tsx")) + .add(append(), LINE_BREAK + """ + describe('App I18next', () => { + it('renders with translation', () => { + vi.mock('react-i18next', () => ({ + useTranslation: () => { + return { + t: vi.fn().mockImplementation((_str: string) => 'Internationalization enabled'), + i18n: { + changeLanguage: () => new Promise(() => {}), + }, + }; + }, + })); + render(); + const { getAllByText } = render(); + const title = getAllByText('Internationalization enabled'); + expect(title).toBeTruthy(); + }); + });""" ) + .and() + .and() + .build(); + //@formatter:off + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java new file mode 100644 index 00000000000..d8fd58c4d30 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java @@ -0,0 +1,27 @@ +package tech.jhipster.lite.generator.client.tools.reacti18n.infrastructure.primary; + +import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.CLIENT_INTERNATIONALIZATION; +import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.REACT_CORE; +import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.REACT_I18N; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.lite.generator.client.tools.reacti18n.application.ReactI18nApplicationService; +import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization; +import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition; +import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource; + +@Configuration +public class ReactI18nModuleConfiguration { + + @Bean + JHipsterModuleResource i18nModule(ReactI18nApplicationService i18n) { + return JHipsterModuleResource.builder() + .slug(REACT_I18N) + .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().build()) + .apiDoc("react i18n", "Add react internationalization") + .organization(JHipsterModuleOrganization.builder().feature(CLIENT_INTERNATIONALIZATION).addDependency(REACT_CORE).build()) + .tags("client", "react", "i18n") + .factory(i18n::buildModule); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/package-info.java new file mode 100644 index 00000000000..06b5c1d8c84 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/package-info.java @@ -0,0 +1,2 @@ +@tech.jhipster.lite.BusinessContext +package tech.jhipster.lite.generator.client.tools.reacti18n; diff --git a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java index 7432d9bc5f9..c358958b55a 100644 --- a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java +++ b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java @@ -8,6 +8,7 @@ public enum JHLiteFeatureSlug implements JHipsterFeatureSlugFactory { AUTHENTICATION_SPRINGDOC("authentication-springdoc"), JCACHE("jcache"), CLIENT_CORE("client-core"), + CLIENT_INTERNATIONALIZATION("client-internationalization"), CUCUMBER_AUTHENTICATION("cucumber-authentication"), DATABASE_MIGRATION("database-migration"), DOCKERFILE("dockerfile"), diff --git a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java index a7326069bf2..ff9f20647ac 100644 --- a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java +++ b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java @@ -46,6 +46,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory { GRADLE_WRAPPER("gradle-wrapper"), HIBERNATE_2ND_LEVEL_CACHE("hibernate-2nd-level-cache"), INFINITEST_FILTERS("infinitest-filters"), + REACT_I18N("react-i18next"), INIT("init"), INTERNATIONALIZED_ERRORS("internationalized-errors"), JACOCO("jacoco"), diff --git a/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java b/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java index b46ccf365e4..768f5f7ae02 100644 --- a/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java +++ b/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java @@ -290,6 +290,10 @@ public static RegexNeedleAfterReplacer lineAfterRegex(String regex) { return new RegexNeedleAfterReplacer(notContainingReplacement(), Pattern.compile(regex, Pattern.MULTILINE)); } + public static EndOfFileReplacer append() { + return new EndOfFileReplacer(ReplacementCondition.always()); + } + public static BuildProfileId buildProfileId(String id) { return new BuildProfileId(id); } diff --git a/src/main/resources/generator/client/common/reacti18n/src/main/webapp/app/i18n.ts.mustache b/src/main/resources/generator/client/common/reacti18n/src/main/webapp/app/i18n.ts.mustache new file mode 100644 index 00000000000..738c25d7376 --- /dev/null +++ b/src/main/resources/generator/client/common/reacti18n/src/main/webapp/app/i18n.ts.mustache @@ -0,0 +1,24 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + + .init({ + fallbackLng: 'en', + debug: false, + + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: '../assets/locales/{{ lng }}/{{ ns }}.json', + }, + }); + +export default i18n; diff --git a/src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/english.json.mustache b/src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/english.json.mustache new file mode 100644 index 00000000000..1d29147104b --- /dev/null +++ b/src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/english.json.mustache @@ -0,0 +1,3 @@ +{ + "translationEnabled": "Internationalization enabled" +} diff --git a/src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/french.json.mustache b/src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/french.json.mustache new file mode 100644 index 00000000000..f4c3ffcea83 --- /dev/null +++ b/src/main/resources/generator/client/common/reacti18n/src/main/webapp/assets/french.json.mustache @@ -0,0 +1,3 @@ +{ + "translationEnabled": "Internationalisation activée" +} diff --git a/src/main/resources/generator/dependencies/react/package.json b/src/main/resources/generator/dependencies/react/package.json index cc7a65d63cc..d9c12706cb8 100644 --- a/src/main/resources/generator/dependencies/react/package.json +++ b/src/main/resources/generator/dependencies/react/package.json @@ -7,9 +7,13 @@ "dependencies": { "@nextui-org/react": "2.4.6", "axios": "1.7.4", + "i18next": "23.14.0", + "i18next-browser-languagedetector": "8.0.0", + "i18next-http-backend": "2.6.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-hook-form": "7.52.2" + "react-hook-form": "7.52.2", + "react-i18next": "15.0.1" }, "devDependencies": { "@testing-library/dom": "10.4.0", diff --git a/src/test/features/client/reacti18n.feature b/src/test/features/client/reacti18n.feature new file mode 100644 index 00000000000..38eaf1d4b93 --- /dev/null +++ b/src/test/features/client/reacti18n.feature @@ -0,0 +1,13 @@ +Feature: React i18n + + Scenario: Should apply react i18n module to react + When I apply modules to default project + | init | + | react-core | + | 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 | diff --git a/src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java new file mode 100644 index 00000000000..30bf5246b7a --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java @@ -0,0 +1,104 @@ +package tech.jhipster.lite.generator.client.tools.reacti18n.domain; + +import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.*; + +import org.junit.jupiter.api.Test; +import tech.jhipster.lite.TestFileUtils; +import tech.jhipster.lite.UnitTest; +import tech.jhipster.lite.module.domain.JHipsterModule; +import tech.jhipster.lite.module.domain.JHipsterModulesFixture; + +@UnitTest +public class ReactI18nModuleFactoryTest { + + public static final ReactI18nModuleFactory factory = new ReactI18nModuleFactory(); + + private static final String APP_TSX = "src/main/webapp/app/common/primary/app/App.tsx"; + + @Test + void shouldBuildI18nModule() { + JHipsterModule module = factory.buildModule( + JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).projectBaseName("jhipster").build() + ); + + JHipsterModuleAsserter asserter = assertThatModuleWithFiles(module, packageJsonFile(), app(), appTest(), index()); + + asserter + .hasFile("package.json") + .containing(nodeDependency("i18next")) + .containing(nodeDependency("i18next-browser-languagedetector")) + .containing(nodeDependency("i18next-http-backend")) + .containing(nodeDependency("react-i18next")) + .and() + .hasFile("src/main/webapp/app/i18n.ts") + .containing( + """ + import i18n from 'i18next'; + import { initReactI18next } from 'react-i18next'; + + import Backend from 'i18next-http-backend'; + import LanguageDetector from 'i18next-browser-languagedetector'; + + i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + + .init({ + fallbackLng: 'en', + debug: false, + + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: '../assets/locales/{{ lng }}/{{ ns }}.json', + }, + }); + + export default i18n; + """ + ) + .and() + .hasFile("src/main/webapp/app/index.tsx") + .containing("import './i18n'") + .and() + .hasFile("src/main/webapp/app/common/primary/app/App.tsx") + .containing("import { useTranslation } from 'react-i18next") + .containing("const { t } = useTranslation();") + .containing("{t('translationEnabled')}") + .and() + .hasFile("src/main/webapp/assets/locales/en/translation.json") + .containing( + """ + { + "translationEnabled": "Internationalization enabled" + } + """ + ) + .and() + .hasFile("src/main/webapp/assets/locales/fr/translation.json") + .containing( + """ + { + "translationEnabled": "Internationalisation activée" + } + """ + ) + .and() + .hasFile("src/test/webapp/unit/common/primary/app/App.spec.tsx") + .containing("describe('App I18next', () => {"); + } + + private ModuleFile app() { + return file("src/test/resources/projects/react-app/App.tsx", APP_TSX); + } + + private ModuleFile appTest() { + return file("src/test/resources/projects/react-app/App.spec.tsx", "src/test/webapp/unit/common/primary/app/App.spec.tsx"); + } + + private ModuleFile index() { + return file("src/test/resources/projects/react-app/index.tsx", "src/main/webapp/app/index.tsx"); + } +} diff --git a/src/test/resources/projects/react-app/App.spec.tsx b/src/test/resources/projects/react-app/App.spec.tsx new file mode 100644 index 00000000000..f5e87d503ed --- /dev/null +++ b/src/test/resources/projects/react-app/App.spec.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react'; +import { describe, it } from 'vitest'; + +import App from '@/common/primary/app/App'; + +describe('App tests', () => { + it('renders without crashing', () => { + const { getByText } = render(); + const title = getByText('React + TypeScript + Vite'); + expect(title).toBeTruthy(); + }); +}); From e9f760fddfc530e469b4a318929c56d4c4955c8c Mon Sep 17 00:00:00 2001 From: fabienpuissant Date: Sat, 24 Aug 2024 03:55:54 +0200 Subject: [PATCH 2/3] PR comments --- .../ReactI18nApplicationService.java | 4 +-- .../i18n}/domain/ReactI18nModuleFactory.java | 25 ++++++++++++------- .../primary/ReactI18nModuleConfiguration.java | 6 ++--- .../i18n}/package-info.java | 0 .../slug/domain/JHLiteModuleSlug.java | 2 +- .../react/i18n/src/main/webapp/app/i18n.ts | 24 ++++++++++++++++++ .../webapp/assets/locales/en/translation.json | 3 +++ .../webapp/assets/locales/fr/translation.json | 3 +++ .../dependencies/common/package.json | 5 ++++ .../generator/dependencies/react/package.json | 3 --- .../domain/ReactI18nModuleFactoryTest.java | 2 +- 11 files changed, 58 insertions(+), 19 deletions(-) rename src/main/java/tech/jhipster/lite/generator/client/{tools/reacti18n => react/i18n}/application/ReactI18nApplicationService.java (75%) rename src/main/java/tech/jhipster/lite/generator/client/{tools/reacti18n => react/i18n}/domain/ReactI18nModuleFactory.java (77%) rename src/main/java/tech/jhipster/lite/generator/client/{tools/reacti18n => react/i18n}/infrastructure/primary/ReactI18nModuleConfiguration.java (81%) rename src/main/java/tech/jhipster/lite/generator/client/{tools/reacti18n => react/i18n}/package-info.java (100%) create mode 100644 src/main/resources/generator/client/react/i18n/src/main/webapp/app/i18n.ts create mode 100644 src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/en/translation.json create mode 100644 src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/fr/translation.json rename src/test/java/tech/jhipster/lite/generator/client/{tools/reacti18n => react/i18n}/domain/ReactI18nModuleFactoryTest.java (97%) diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/application/ReactI18nApplicationService.java similarity index 75% rename from src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java rename to src/main/java/tech/jhipster/lite/generator/client/react/i18n/application/ReactI18nApplicationService.java index e9417c5e1d1..588a845c9a7 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/application/ReactI18nApplicationService.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/application/ReactI18nApplicationService.java @@ -1,7 +1,7 @@ -package tech.jhipster.lite.generator.client.tools.reacti18n.application; +package tech.jhipster.lite.generator.client.react.i18n.application; import org.springframework.stereotype.Service; -import tech.jhipster.lite.generator.client.tools.reacti18n.domain.ReactI18nModuleFactory; +import tech.jhipster.lite.generator.client.react.i18n.domain.ReactI18nModuleFactory; import tech.jhipster.lite.module.domain.JHipsterModule; import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties; diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java similarity index 77% rename from src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java rename to src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java index a1e6ed835b2..c7043fcb28f 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactory.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactory.java @@ -1,4 +1,4 @@ -package tech.jhipster.lite.generator.client.tools.reacti18n.domain; +package tech.jhipster.lite.generator.client.react.i18n.domain; import static tech.jhipster.lite.module.domain.JHipsterModule.*; @@ -10,8 +10,9 @@ public class ReactI18nModuleFactory { - private static final JHipsterSource APP_SOURCE = from("client/common/reacti18n/src/main/webapp/app"); - private static final JHipsterSource ASSETS_SOURCE = from("client/common/reacti18n/src/main/webapp/assets"); + 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 String INDEX = "src/main/webapp/"; private static final String INDEX_TEST = "src/test/webapp/unit/common/primary/app/"; @@ -22,15 +23,21 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { //@formatter:off return moduleBuilder(properties) .packageJson() - .addDependency(packageName("i18next"), VersionSource.REACT) - .addDependency(packageName("i18next-browser-languagedetector"), VersionSource.REACT) - .addDependency(packageName("i18next-http-backend"), VersionSource.REACT) + .addDependency(packageName("i18next"), VersionSource.COMMON) + .addDependency(packageName("i18next-browser-languagedetector"), VersionSource.COMMON) + .addDependency(packageName("i18next-http-backend"), VersionSource.COMMON) .addDependency(packageName("react-i18next"), VersionSource.REACT) .and() .files() - .add(APP_SOURCE.template("i18n.ts"), to(INDEX + "app/i18n.ts")) - .add(ASSETS_SOURCE.template("english.json"), to(INDEX + "assets/locales/en/translation.json")) - .add(ASSETS_SOURCE.template("french.json"), to(INDEX + "assets/locales/fr/translation.json")) + .batch(APP_SOURCE, to(INDEX + "/app")) + .addFile("i18n.ts") + .and() + .batch(ASSETS_EN_SOURCE, to(INDEX + "assets/locales/en/")) + .addFile("translation.json") + .and() + .batch(ASSETS_FR_SOURCE, to(INDEX + "assets/locales/fr/")) + .addFile("translation.json") + .and() .and() .mandatoryReplacements() .in(path(INDEX + "app/common/primary/app/App.tsx")) diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java similarity index 81% rename from src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java rename to src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java index d8fd58c4d30..f734818cd31 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/infrastructure/primary/ReactI18nModuleConfiguration.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java @@ -1,4 +1,4 @@ -package tech.jhipster.lite.generator.client.tools.reacti18n.infrastructure.primary; +package tech.jhipster.lite.generator.client.react.i18n.infrastructure.primary; import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.CLIENT_INTERNATIONALIZATION; import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.REACT_CORE; @@ -6,7 +6,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import tech.jhipster.lite.generator.client.tools.reacti18n.application.ReactI18nApplicationService; +import tech.jhipster.lite.generator.client.react.i18n.application.ReactI18nApplicationService; import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization; import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition; import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource; @@ -19,7 +19,7 @@ JHipsterModuleResource i18nModule(ReactI18nApplicationService i18n) { return JHipsterModuleResource.builder() .slug(REACT_I18N) .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().build()) - .apiDoc("react i18n", "Add react internationalization") + .apiDoc("Frontend - React", "Add react internationalization") .organization(JHipsterModuleOrganization.builder().feature(CLIENT_INTERNATIONALIZATION).addDependency(REACT_CORE).build()) .tags("client", "react", "i18n") .factory(i18n::buildModule); diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/package-info.java similarity index 100% rename from src/main/java/tech/jhipster/lite/generator/client/tools/reacti18n/package-info.java rename to src/main/java/tech/jhipster/lite/generator/client/react/i18n/package-info.java diff --git a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java index ff9f20647ac..5e637bc017b 100644 --- a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java +++ b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java @@ -46,7 +46,6 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory { GRADLE_WRAPPER("gradle-wrapper"), HIBERNATE_2ND_LEVEL_CACHE("hibernate-2nd-level-cache"), INFINITEST_FILTERS("infinitest-filters"), - REACT_I18N("react-i18next"), INIT("init"), INTERNATIONALIZED_ERRORS("internationalized-errors"), JACOCO("jacoco"), @@ -75,6 +74,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory { MAVEN_WRAPPER("maven-wrapper"), MONGOCK("mongock"), MONGODB("mongodb"), + REACT_I18N("react-i18next"), REDIS("redis"), MSSQL("mssql"), MYSQL("mysql"), diff --git a/src/main/resources/generator/client/react/i18n/src/main/webapp/app/i18n.ts b/src/main/resources/generator/client/react/i18n/src/main/webapp/app/i18n.ts new file mode 100644 index 00000000000..738c25d7376 --- /dev/null +++ b/src/main/resources/generator/client/react/i18n/src/main/webapp/app/i18n.ts @@ -0,0 +1,24 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + + .init({ + fallbackLng: 'en', + debug: false, + + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: '../assets/locales/{{ lng }}/{{ ns }}.json', + }, + }); + +export default i18n; diff --git a/src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/en/translation.json b/src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/en/translation.json new file mode 100644 index 00000000000..1d29147104b --- /dev/null +++ b/src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/en/translation.json @@ -0,0 +1,3 @@ +{ + "translationEnabled": "Internationalization enabled" +} diff --git a/src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/fr/translation.json b/src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/fr/translation.json new file mode 100644 index 00000000000..f4c3ffcea83 --- /dev/null +++ b/src/main/resources/generator/client/react/i18n/src/main/webapp/assets/locales/fr/translation.json @@ -0,0 +1,3 @@ +{ + "translationEnabled": "Internationalisation activée" +} diff --git a/src/main/resources/generator/dependencies/common/package.json b/src/main/resources/generator/dependencies/common/package.json index 8b3b8a33f94..0fef04e77d4 100644 --- a/src/main/resources/generator/dependencies/common/package.json +++ b/src/main/resources/generator/dependencies/common/package.json @@ -3,6 +3,11 @@ "version": "0.0.0", "description": "JHipster Lite : used for Dependencies", "license": "Apache-2.0", + "dependencies": { + "i18next": "23.14.0", + "i18next-browser-languagedetector": "8.0.0", + "i18next-http-backend": "2.6.1" + }, "devDependencies": { "@babel/cli": "7.24.8", "@playwright/test": "1.46.1", diff --git a/src/main/resources/generator/dependencies/react/package.json b/src/main/resources/generator/dependencies/react/package.json index d9c12706cb8..0ee7c85f9fe 100644 --- a/src/main/resources/generator/dependencies/react/package.json +++ b/src/main/resources/generator/dependencies/react/package.json @@ -7,9 +7,6 @@ "dependencies": { "@nextui-org/react": "2.4.6", "axios": "1.7.4", - "i18next": "23.14.0", - "i18next-browser-languagedetector": "8.0.0", - "i18next-http-backend": "2.6.1", "react": "18.3.1", "react-dom": "18.3.1", "react-hook-form": "7.52.2", diff --git a/src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java similarity index 97% rename from src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java rename to src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java index 30bf5246b7a..6ef3d826463 100644 --- a/src/test/java/tech/jhipster/lite/generator/client/tools/reacti18n/domain/ReactI18nModuleFactoryTest.java +++ b/src/test/java/tech/jhipster/lite/generator/client/react/i18n/domain/ReactI18nModuleFactoryTest.java @@ -1,4 +1,4 @@ -package tech.jhipster.lite.generator.client.tools.reacti18n.domain; +package tech.jhipster.lite.generator.client.react.i18n.domain; import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.*; From e4a3365dc2435e850ff18910a55e3f1c2ab10ee2 Mon Sep 17 00:00:00 2001 From: fabienpuissant Date: Sun, 25 Aug 2024 02:56:34 +0200 Subject: [PATCH 3/3] PR comments --- .../primary/ReactI18nModuleConfiguration.java | 2 +- .../client/react/i18n/package-info.java | 2 +- .../slug/domain/JHLiteModuleSlug.java | 4 +-- .../npm/FileSystemNpmVersionReaderTest.java | 25 +++++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java index f734818cd31..4b245b4f8f5 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/infrastructure/primary/ReactI18nModuleConfiguration.java @@ -12,7 +12,7 @@ import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource; @Configuration -public class ReactI18nModuleConfiguration { +class ReactI18nModuleConfiguration { @Bean JHipsterModuleResource i18nModule(ReactI18nApplicationService i18n) { diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/i18n/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/package-info.java index 06b5c1d8c84..b0d7de1f18a 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/react/i18n/package-info.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/i18n/package-info.java @@ -1,2 +1,2 @@ @tech.jhipster.lite.BusinessContext -package tech.jhipster.lite.generator.client.tools.reacti18n; +package tech.jhipster.lite.generator.client.react.i18n; diff --git a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java index 5e637bc017b..70958adfb86 100644 --- a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java +++ b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java @@ -74,8 +74,6 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory { MAVEN_WRAPPER("maven-wrapper"), MONGOCK("mongock"), MONGODB("mongodb"), - REACT_I18N("react-i18next"), - REDIS("redis"), MSSQL("mssql"), MYSQL("mysql"), NEO4J("neo4j"), @@ -88,7 +86,9 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory { PROTOBUF("protobuf"), PROTOBUF_BACKWARDS_COMPATIBILITY_CHECK("protobuf-backwards-compatibility-check"), REACT_CORE("react-core"), + REACT_I18N("react-i18next"), REACT_JWT("react-jwt"), + REDIS("redis"), REST_PAGINATION("rest-pagination"), SAMPLE_CASSANDRA_PERSISTENCE("sample-cassandra-persistence"), SAMPLE_FEATURE("sample-feature"), diff --git a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/npm/FileSystemNpmVersionReaderTest.java b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/npm/FileSystemNpmVersionReaderTest.java index 3060383d862..5d28245b12c 100644 --- a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/npm/FileSystemNpmVersionReaderTest.java +++ b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/npm/FileSystemNpmVersionReaderTest.java @@ -42,6 +42,15 @@ void shouldGetVersionFromSource() { assertThat(version).isEqualTo(new NpmPackageVersion("1.2.3")); } + @Test + void shouldGetVersionFromEmptySourceWithEmptyDevSource() { + emptyProjectFiles(); + + NpmPackageVersion version = reader.get().get(new NpmPackageName("vue"), NpmVersionSource.COMMON); + + assertThat(version).isEqualTo(new NpmPackageVersion("1.2.3")); + } + private void mockProjectFiles() { when(projectFiles.readString(anyString())).thenReturn( """ @@ -80,4 +89,20 @@ private void mockProjectFiles() { """ ); } + + private void emptyProjectFiles() { + when(projectFiles.readString(anyString())).thenReturn( + """ + { + "name": "jhlite-dependencies", + "version": "0.0.0", + "description": "JHipster Lite : used for Dependencies", + "license": "Apache-2.0", + "dependencies": { + "vue": "1.2.3" + }, + } + """ + ); + } }