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 4b245b4f8f5..18eafdb0af3 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
@@ -15,7 +15,7 @@
class ReactI18nModuleConfiguration {
@Bean
- JHipsterModuleResource i18nModule(ReactI18nApplicationService i18n) {
+ JHipsterModuleResource reactI18nModule(ReactI18nApplicationService i18n) {
return JHipsterModuleResource.builder()
.slug(REACT_I18N)
.propertiesDefinition(JHipsterModulePropertiesDefinition.builder().build())
diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/application/VueI18nApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/application/VueI18nApplicationService.java
new file mode 100644
index 00000000000..f669a15ac20
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/application/VueI18nApplicationService.java
@@ -0,0 +1,20 @@
+package tech.jhipster.lite.generator.client.vue.i18n.application;
+
+import org.springframework.stereotype.Service;
+import tech.jhipster.lite.generator.client.vue.i18n.domain.VueI18nModuleFactory;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@Service
+public class VueI18nApplicationService {
+
+ private final VueI18nModuleFactory factory;
+
+ public VueI18nApplicationService() {
+ factory = new VueI18nModuleFactory();
+ }
+
+ public JHipsterModule buildModule(JHipsterModuleProperties properties) {
+ return factory.buildModule(properties);
+ }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/domain/VueI18nModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/domain/VueI18nModuleFactory.java
new file mode 100644
index 00000000000..339bf234ed5
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/domain/VueI18nModuleFactory.java
@@ -0,0 +1,74 @@
+package tech.jhipster.lite.generator.client.vue.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 VueI18nModuleFactory {
+
+ private static final JHipsterSource APP_SOURCE = from("client/common/i18n");
+ private static final JHipsterSource COMMON_CONTEXT_SOURCE = from("client/common/i18n/app");
+ private static final JHipsterSource ASSETS_SOURCE = from("client/common/i18n/app/locales");
+ private static final JHipsterSource TEST_SOURCE = from("client/vue/i18n/src/test");
+
+ 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);
+
+ //@formatter:off
+ return moduleBuilder(properties)
+ .packageJson()
+ .addDependency(packageName("i18next"), VersionSource.COMMON)
+ .addDependency(packageName("i18next-vue"), VersionSource.VUE)
+ .addDependency(packageName("i18next-browser-languagedetector"), VersionSource.COMMON)
+ .and()
+ .files()
+ .batch(APP_SOURCE, to(INDEX))
+ .addFile("i18n.ts")
+ .addFile("Translations.ts")
+ .and()
+ .batch(COMMON_CONTEXT_SOURCE, to(INDEX + "common/"))
+ .addFile("CommonTranslations.ts")
+ .and()
+ .batch(ASSETS_SOURCE, to(INDEX + "common/locales/"))
+ .addFile("en.ts")
+ .addFile("fr.ts")
+ .and()
+ .batch(TEST_SOURCE, to(INDEX_TEST))
+ .addFile("setupTests.ts")
+ .and()
+ .and()
+ .mandatoryReplacements()
+ .in(path(INDEX + "main.ts"))
+ .add(lineBeforeText("// jhipster-needle-main-ts-import"), "import i18next from './i18n';")
+ .add(lineBeforeText("// jhipster-needle-main-ts-import"), "import I18NextVue from 'i18next-vue';")
+ .add(lineBeforeText("app.mount('#app');"), "app.use(I18NextVue, { i18next });")
+ .and()
+ .in(path(INDEX + "/common/primary/homepage/Homepage.html"))
+ .add(lineAfterRegex("Vue 3 \\+ TypeScript \\+ Vite"), properties.indentation().times(1) + "
")
+ .and()
+ .in(path("./vitest.config.ts"))
+ .add(lineAfterRegex("test:"), properties.indentation().times(2) + "setupFiles: ['./src/test/setupTests.ts'],")
+ .and()
+ .in(path(INDEX_TEST + "webapp/unit/common/primary/homepage/Homepage.spec.ts"))
+ .add(append(), LINE_BREAK + """
+ describe('App I18next', () => {
+ it('should renders with translation', () => {
+ wrap();
+
+ expect(wrapper.text()).toContain("translationEnabled");
+ });
+ });
+ """)
+ .and()
+ .and()
+ .build();
+ //@formatter:off
+ }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/infrastructure/primary/VueI18nModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/infrastructure/primary/VueI18nModuleConfiguration.java
new file mode 100644
index 00000000000..6f9614c7fab
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/infrastructure/primary/VueI18nModuleConfiguration.java
@@ -0,0 +1,27 @@
+package tech.jhipster.lite.generator.client.vue.i18n.infrastructure.primary;
+
+import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.CLIENT_INTERNATIONALIZATION;
+import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.VUE_CORE;
+import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.VUE_I18N;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tech.jhipster.lite.generator.client.vue.i18n.application.VueI18nApplicationService;
+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 VueI18nModuleConfiguration {
+
+ @Bean
+ JHipsterModuleResource vueI18nModule(VueI18nApplicationService i18n) {
+ return JHipsterModuleResource.builder()
+ .slug(VUE_I18N)
+ .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().build())
+ .apiDoc("Frontend - Vue", "Add vue internationalization")
+ .organization(JHipsterModuleOrganization.builder().feature(CLIENT_INTERNATIONALIZATION).addDependency(VUE_CORE).build())
+ .tags("client", "vue", "i18n")
+ .factory(i18n::buildModule);
+ }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/package-info.java
new file mode 100644
index 00000000000..4cc0fb5bcad
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/vue/i18n/package-info.java
@@ -0,0 +1,2 @@
+@tech.jhipster.lite.BusinessContext
+package tech.jhipster.lite.generator.client.vue.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 c8e21fef708..3e9531dd060 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
@@ -147,6 +147,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
SVELTE_CORE("svelte-core"),
TYPESCRIPT("typescript"),
VUE_CORE("vue-core"),
+ VUE_I18N("vue-i18next"),
VUE_PINIA("vue-pinia"),
TS_PAGINATION_DOMAIN("ts-pagination-domain"),
TS_REST_PAGINATION("ts-rest-pagination");
diff --git a/src/main/resources/generator/client/common/i18n/Translations.ts b/src/main/resources/generator/client/common/i18n/Translations.ts
new file mode 100644
index 00000000000..da7464b310b
--- /dev/null
+++ b/src/main/resources/generator/client/common/i18n/Translations.ts
@@ -0,0 +1,33 @@
+import type { Resource, ResourceKey, ResourceLanguage } from 'i18next';
+
+export type Translation = Record;
+export type Translations = Record;
+
+const toLanguage = ([key, value]: [string, ResourceKey]): [string, ResourceLanguage] => [
+ key,
+ {
+ translation: value,
+ },
+];
+
+const mergeTranslations = (translations: Array): Translations =>
+ translations
+ .flatMap(translations => Object.entries(translations))
+ .reduce(
+ (acc, [key, translation]) => ({
+ ...acc,
+ [key]: acc[key] ? { ...acc[key], ...translation } : translation,
+ }),
+ {} as Translations,
+ );
+
+export const toTranslationResources = (...translations: Array): Resource =>
+ Object.entries(mergeTranslations(translations))
+ .map(toLanguage)
+ .reduce(
+ (acc, current) => ({
+ ...acc,
+ [current[0]]: current[1],
+ }),
+ {},
+ );
diff --git a/src/main/resources/generator/client/common/i18n/app/CommonTranslations.ts b/src/main/resources/generator/client/common/i18n/app/CommonTranslations.ts
new file mode 100644
index 00000000000..1fc95c79f39
--- /dev/null
+++ b/src/main/resources/generator/client/common/i18n/app/CommonTranslations.ts
@@ -0,0 +1,5 @@
+import type { Translations } from '@/Translations';
+import { en } from './locales/en';
+import { fr } from './locales/fr';
+
+export const commonTranslations: Translations = { fr, en };
diff --git a/src/main/resources/generator/client/common/i18n/app/locales/en.ts b/src/main/resources/generator/client/common/i18n/app/locales/en.ts
new file mode 100644
index 00000000000..5eb7d1ced1f
--- /dev/null
+++ b/src/main/resources/generator/client/common/i18n/app/locales/en.ts
@@ -0,0 +1,7 @@
+import type { Translation } from '@/Translations';
+
+export const en: Translation = {
+ common: {
+ translationEnabled: 'Internationalization enabled',
+ },
+};
diff --git a/src/main/resources/generator/client/common/i18n/app/locales/fr.ts b/src/main/resources/generator/client/common/i18n/app/locales/fr.ts
new file mode 100644
index 00000000000..7e91dd9b183
--- /dev/null
+++ b/src/main/resources/generator/client/common/i18n/app/locales/fr.ts
@@ -0,0 +1,7 @@
+import type { Translation } from '@/Translations';
+
+export const fr: Translation = {
+ common: {
+ translationEnabled: 'Internationalisation activée',
+ },
+};
diff --git a/src/main/resources/generator/client/common/i18n/i18n.ts b/src/main/resources/generator/client/common/i18n/i18n.ts
new file mode 100644
index 00000000000..cafa5361757
--- /dev/null
+++ b/src/main/resources/generator/client/common/i18n/i18n.ts
@@ -0,0 +1,15 @@
+import i18n from 'i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import { commonTranslations } from './common/CommonTranslations';
+import { toTranslationResources } from './Translations';
+
+i18n.use(LanguageDetector).init({
+ fallbackLng: 'en',
+ debug: false,
+ interpolation: {
+ escapeValue: false,
+ },
+ resources: toTranslationResources(commonTranslations),
+});
+
+export default i18n;
diff --git a/src/main/resources/generator/client/vue/i18n/src/test/setupTests.ts b/src/main/resources/generator/client/vue/i18n/src/test/setupTests.ts
new file mode 100644
index 00000000000..f4d4b283459
--- /dev/null
+++ b/src/main/resources/generator/client/vue/i18n/src/test/setupTests.ts
@@ -0,0 +1,5 @@
+import { config } from '@vue/test-utils';
+
+config.global.mocks = {
+ $t: msg => msg,
+};
diff --git a/src/main/resources/generator/dependencies/vue/package.json b/src/main/resources/generator/dependencies/vue/package.json
index 3e531107ce4..6d58c6526ca 100644
--- a/src/main/resources/generator/dependencies/vue/package.json
+++ b/src/main/resources/generator/dependencies/vue/package.json
@@ -5,6 +5,7 @@
"license": "Apache-2.0",
"dependencies": {
"axios": "1.7.7",
+ "i18next-vue": "5.0.0",
"pinia": "2.2.2",
"pinia-plugin-persistedstate": "3.2.3",
"vue": "3.4.38",
diff --git a/src/test/features/client/vue-i18n.feature b/src/test/features/client/vue-i18n.feature
new file mode 100644
index 00000000000..47c30c6f704
--- /dev/null
+++ b/src/test/features/client/vue-i18n.feature
@@ -0,0 +1,17 @@
+Feature: Vue i18n
+
+ Scenario: Should apply vue i18n module to vue
+ When I apply modules to default project
+ | init |
+ | vue-core |
+ | vue-i18next |
+ Then I should have files in "src/main/webapp/app"
+ | i18n.ts |
+ | Translations.ts |
+ And I should have files in "src/main/webapp/app/common/"
+ | CommonTranslations.ts |
+ And I should have files in "src/main/webapp/app/common/locales"
+ | fr.ts |
+ | en.ts |
+ And I should have files in "src/test"
+ | setupTests.ts |
diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/i18n/domain/VueI18nModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/vue/i18n/domain/VueI18nModuleFactoryTest.java
new file mode 100644
index 00000000000..d6ff150fa49
--- /dev/null
+++ b/src/test/java/tech/jhipster/lite/generator/client/vue/i18n/domain/VueI18nModuleFactoryTest.java
@@ -0,0 +1,177 @@
+package tech.jhipster.lite.generator.client.vue.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
+class VueI18nModuleFactoryTest {
+
+ private static final VueI18nModuleFactory factory = new VueI18nModuleFactory();
+
+ @Test
+ void shouldBuildI18nModule() {
+ JHipsterModule module = factory.buildModule(
+ JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).projectBaseName("jhipster").build()
+ );
+
+ JHipsterModuleAsserter asserter = assertThatModuleWithFiles(
+ module,
+ packageJsonFile(),
+ mainFile(),
+ homepage(),
+ homepageTest(),
+ vitest()
+ );
+ asserter
+ .hasFile("package.json")
+ .containing(nodeDependency("i18next"))
+ .containing(nodeDependency("i18next-vue"))
+ .containing(nodeDependency("i18next-browser-languagedetector"))
+ .and()
+ .hasFile("src/main/webapp/app/i18n.ts")
+ .containing(
+ """
+ import i18n from 'i18next';
+ import LanguageDetector from 'i18next-browser-languagedetector';
+ import { commonTranslations } from './common/CommonTranslations';
+ import { toTranslationResources } from './Translations';
+
+ i18n.use(LanguageDetector).init({
+ fallbackLng: 'en',
+ debug: false,
+ interpolation: {
+ escapeValue: false,
+ },
+ resources: toTranslationResources(commonTranslations),
+ });
+
+ export default i18n;
+ """
+ )
+ .and()
+ .hasFile("src/main/webapp/app/Translations.ts")
+ .containing(
+ """
+ import type { Resource, ResourceKey, ResourceLanguage } from 'i18next';
+
+ export type Translation = Record;
+ export type Translations = Record;
+
+ const toLanguage = ([key, value]: [string, ResourceKey]): [string, ResourceLanguage] => [
+ key,
+ {
+ translation: value,
+ },
+ ];
+
+ const mergeTranslations = (translations: Array): Translations =>
+ translations
+ .flatMap(translations => Object.entries(translations))
+ .reduce(
+ (acc, [key, translation]) => ({
+ ...acc,
+ [key]: acc[key] ? { ...acc[key], ...translation } : translation,
+ }),
+ {} as Translations,
+ );
+
+ export const toTranslationResources = (...translations: Array): Resource =>
+ Object.entries(mergeTranslations(translations))
+ .map(toLanguage)
+ .reduce(
+ (acc, current) => ({
+ ...acc,
+ [current[0]]: current[1],
+ }),
+ {},
+ );
+ """
+ )
+ .and()
+ .hasFile("src/main/webapp/app/common/CommonTranslations.ts")
+ .containing(
+ """
+ import type { Translations } from '@/Translations';
+ import { en } from './locales/en';
+ import { fr } from './locales/fr';
+
+ export const commonTranslations: Translations = { fr, en };
+ """
+ )
+ .and()
+ .hasFile("src/main/webapp/app/main.ts")
+ .containing("import i18next from './i18n';")
+ .containing("import I18NextVue from 'i18next-vue';")
+ .containing("app.use(I18NextVue, { i18next });")
+ .and()
+ .hasFile("src/main/webapp/app/common/primary/homepage/Homepage.html")
+ .containing("")
+ .and()
+ .hasFile("src/test/setupTests.ts")
+ .containing(
+ """
+ import { config } from '@vue/test-utils';
+
+ config.global.mocks = {
+ $t: msg => msg,
+ };
+ """
+ )
+ .and()
+ .hasFile("vitest.config.ts")
+ .containing("setupFiles: ['./src/test/setupTests.ts']")
+ .and()
+ .hasFile("src/main/webapp/app/common/locales/en.ts")
+ .containing(
+ """
+ import type { Translation } from '@/Translations';
+
+ export const en: Translation = {
+ common: {
+ translationEnabled: 'Internationalization enabled',
+ },
+ };
+ """
+ )
+ .and()
+ .hasFile("src/main/webapp/app/common/locales/fr.ts")
+ .containing(
+ """
+ import type { Translation } from '@/Translations';
+
+ export const fr: Translation = {
+ common: {
+ translationEnabled: 'Internationalisation activée',
+ },
+ };
+ """
+ )
+ .and()
+ .hasFile("src/test/webapp/unit/common/primary/homepage/Homepage.spec.ts")
+ .containing("describe('App I18next', () => {");
+ }
+
+ private ModuleFile mainFile() {
+ return file("src/test/resources/projects/vue/main.ts.template", "src/main/webapp/app/main.ts");
+ }
+
+ private ModuleFile homepage() {
+ return file("src/test/resources/projects/vue/Homepage.html.template", "src/main/webapp/app/common/primary/homepage/Homepage.html");
+ }
+
+ private ModuleFile homepageTest() {
+ return file(
+ "src/test/resources/projects/vue/Homepage.spec.ts.template",
+ "src/test/webapp/unit/common/primary/homepage/Homepage.spec.ts"
+ );
+ }
+
+ private ModuleFile vitest() {
+ return file("src/test/resources/projects/vue/vitest.config.ts.template", "./vitest.config.ts");
+ }
+}
diff --git a/src/test/resources/projects/vue/Homepage.html.template b/src/test/resources/projects/vue/Homepage.html.template
new file mode 100644
index 00000000000..bbf89ea7c45
--- /dev/null
+++ b/src/test/resources/projects/vue/Homepage.html.template
@@ -0,0 +1,23 @@
+
+
+
+
+
{{ appName }}: Vue 3 + TypeScript + Vite
+
+ Recommended IDE setup:
+ VSCode
+ +
+ Volar
+
+
+
+ Vite Documentation
+ |
+ Vue 3 Documentation
+
+
+
+ Edit
+ src/main/webapp/app/common/primary/app/AppVue.vue
to test hot module replacement.
+
+
diff --git a/src/test/resources/projects/vue/Homepage.spec.ts.template b/src/test/resources/projects/vue/Homepage.spec.ts.template
new file mode 100644
index 00000000000..2932aad09ed
--- /dev/null
+++ b/src/test/resources/projects/vue/Homepage.spec.ts.template
@@ -0,0 +1,17 @@
+import { describe, it, expect } from 'vitest';
+import { shallowMount, VueWrapper } from '@vue/test-utils';
+import { HomepageVue } from '@/common/primary/homepage';
+
+let wrapper: VueWrapper;
+
+const wrap = () => {
+ wrapper = shallowMount(HomepageVue);
+};
+
+describe('App', () => {
+ it('should exist', () => {
+ wrap();
+
+ expect(wrapper.exists()).toBeTruthy();
+ });
+});
diff --git a/src/test/resources/projects/vue/vitest.config.ts.template b/src/test/resources/projects/vue/vitest.config.ts.template
new file mode 100644
index 00000000000..fa16dbb0df7
--- /dev/null
+++ b/src/test/resources/projects/vue/vitest.config.ts.template
@@ -0,0 +1,65 @@
+import { defineConfig } from 'vitest/config';
+import path from 'path';
+import vue from '@vitejs/plugin-vue';
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src/main/webapp/app'),
+ },
+ },
+ plugins: [
+ vue({
+ template: {
+ compilerOptions: {
+ isCustomElement: tag => /^x-/.test(tag),
+ },
+ },
+ }),
+ ],
+ test: {
+ include: ['src/test/webapp/unit/**/*.spec.ts'],
+ logHeapUsage: true,
+ poolOptions: {
+ threads: {
+ minThreads: 1,
+ maxThreads: 2,
+ },
+ },
+ environment: 'jsdom',
+ cache: false,
+ reporters: ['default', 'vitest-sonar-reporter'],
+ outputFile: {
+ 'vitest-sonar-reporter': 'target/test-results/TESTS-results-sonar.xml',
+ },
+ coverage: {
+ all: false,
+ clean: true,
+ thresholds: {
+ perFile: true,
+ autoUpdate: true,
+ 100: true,
+ },
+ exclude: [
+ 'src/main/webapp/**/*.component.ts',
+ 'src/main/webapp/app/main.ts',
+ 'src/main/webapp/app/router/index.ts',
+ 'src/main/webapp/app/**/application/*Provider.ts',
+ 'src/main/webapp/app/shared/alert/infrastructure/primary/WindowApplicationListener.ts',
+ 'src/main/webapp/app/module/secondary/RestManagementRepository.ts',
+ 'src/main/webapp/app/injections.ts',
+ '**/*.d.ts',
+ 'src/test/**/*',
+ ],
+ provider: 'istanbul',
+ reportsDirectory: 'target/test-results/',
+ reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+ watermarks: {
+ statements: [100, 100],
+ branches: [100, 100],
+ functions: [100, 100],
+ lines: [100, 100],
+ },
+ },
+ },
+});