Skip to content

Commit

Permalink
add react internationalization (#10639)
Browse files Browse the repository at this point in the history
create react internalization module using i18next with language detector
see https://react.i18next.com/latest/using-with-hooks
  • Loading branch information
fabienpuissant authored Aug 25, 2024
1 parent 2454d7e commit f8efc0d
Show file tree
Hide file tree
Showing 19 changed files with 354 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tech.jhipster.lite.generator.client.react.i18n.application;

import org.springframework.stereotype.Service;
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;

@Service
public class ReactI18nApplicationService {

private final ReactI18nModuleFactory factory;

public ReactI18nApplicationService() {
factory = new ReactI18nModuleFactory();
}

public JHipsterModule buildModule(JHipsterModuleProperties properties) {
return factory.buildModule(properties);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package tech.jhipster.lite.generator.client.react.i18n.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/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/";

public JHipsterModule buildModule(JHipsterModuleProperties properties) {
Assert.notNull("properties", properties);

//@formatter:off
return moduleBuilder(properties)
.packageJson()
.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()
.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"))
.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("</h1>"), LINE_BREAK +
properties.indentation().times(4) + "<p>{t('translationEnabled')}</p>")
.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(<App />);
const { getAllByText } = render(<App />);
const title = getAllByText('Internationalization enabled');
expect(title).toBeTruthy();
});
});""" )
.and()
.and()
.build();
//@formatter:off
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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;
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.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;

@Configuration
class ReactI18nModuleConfiguration {

@Bean
JHipsterModuleResource i18nModule(ReactI18nApplicationService i18n) {
return JHipsterModuleResource.builder()
.slug(REACT_I18N)
.propertiesDefinition(JHipsterModulePropertiesDefinition.builder().build())
.apiDoc("Frontend - React", "Add react internationalization")
.organization(JHipsterModuleOrganization.builder().feature(CLIENT_INTERNATIONALIZATION).addDependency(REACT_CORE).build())
.tags("client", "react", "i18n")
.factory(i18n::buildModule);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@tech.jhipster.lite.BusinessContext
package tech.jhipster.lite.generator.client.react.i18n;
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
MAVEN_WRAPPER("maven-wrapper"),
MONGOCK("mongock"),
MONGODB("mongodb"),
REDIS("redis"),
MSSQL("mssql"),
MYSQL("mysql"),
NEO4J("neo4j"),
Expand All @@ -89,7 +88,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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"translationEnabled": "Internationalization enabled"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"translationEnabled": "Internationalisation activée"
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"translationEnabled": "Internationalization enabled"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"translationEnabled": "Internationalisation activée"
}
5 changes: 5 additions & 0 deletions src/main/resources/generator/dependencies/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/generator/dependencies/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"axios": "1.7.4",
"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",
Expand Down
13 changes: 13 additions & 0 deletions src/test/features/client/reacti18n.feature
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package tech.jhipster.lite.generator.client.react.i18n.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");
}
}
Loading

0 comments on commit f8efc0d

Please sign in to comment.