Skip to content

Commit

Permalink
Merge pull request #10690 from fabienpuissant/feature/vue-internation…
Browse files Browse the repository at this point in the history
…alization

add vue internationalization
  • Loading branch information
murdos committed Sep 27, 2024
2 parents 2b6243a + 6475496 commit 41cd23f
Show file tree
Hide file tree
Showing 19 changed files with 466 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package tech.jhipster.lite.generator.client.vue.i18n.domain;

import static tech.jhipster.lite.module.domain.JHipsterModule.*;
import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.COMMON;
import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.VUE;

import tech.jhipster.lite.module.domain.JHipsterModule;
import tech.jhipster.lite.module.domain.file.JHipsterSource;
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 HOME_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/";

private static final String PROVIDER_NEEDLE = "// jhipster-needle-main-ts-provider";

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

//@formatter:off
return moduleBuilder(properties)
.packageJson()
.addDependency(packageName("i18next"), COMMON)
.addDependency(packageName("i18next-vue"), VUE)
.addDependency(packageName("i18next-browser-languagedetector"), COMMON)
.and()
.files()
.batch(APP_SOURCE, to(INDEX))
.addFile("i18n.ts")
.addFile("Translations.ts")
.and()
.batch(HOME_CONTEXT_SOURCE, to(INDEX + "home/"))
.addFile("HomeTranslations.ts")
.and()
.batch(ASSETS_SOURCE, to(INDEX + "home/locales/"))
.addFile("en.ts")
.addFile("fr.ts")
.and()
.batch(TEST_SOURCE, to(INDEX_TEST))
.addFile("setupTests.ts")
.and()
.batch(TEST_SOURCE, to(INDEX_TEST + "webapp/unit/home/infrastructure/primary/"))
.addFile("HomePageVue.spec.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(PROVIDER_NEEDLE
), "app.use(I18NextVue, { i18next });")
.and()
.in(path(INDEX + "/home/infrastructure/primary/HomepageVue.vue"))
.add(lineAfterRegex("Vue 3 \\+ TypeScript \\+ Vite"), properties.indentation().times(2) + "<h2 v-html=\"$t('home.translationEnabled')\"></h2>")
.and()
.in(path("./vitest.config.ts"))
.add(lineAfterRegex("test:"), properties.indentation().times(2) + "setupFiles: ['./src/test/setupTests.ts'],")
.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.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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@tech.jhipster.lite.BusinessContext
package tech.jhipster.lite.generator.client.vue.i18n;
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
SVELTE_CORE("svelte-core"),
TYPESCRIPT("typescript"),
VUE_CORE("vue-core"),
VUE_I18N("vue-i18next"),
VUE_OAUTH2_KEYCLOAK("vue-oauth2-keycloak"),
VUE_PINIA("vue-pinia"),
TS_PAGINATION_DOMAIN("ts-pagination-domain"),
Expand Down
33 changes: 33 additions & 0 deletions src/main/resources/generator/client/common/i18n/Translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Resource, ResourceKey, ResourceLanguage } from 'i18next';

export type Translation = Record<string, unknown>;
export type Translations = Record<string, Translation>;

const toLanguage = ([key, value]: [string, ResourceKey]): [string, ResourceLanguage] => [
key,
{
translation: value,
},
];

const mergeTranslations = (translations: Translations[]): 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: Translations[]): Resource =>
Object.entries(mergeTranslations(translations))
.map(toLanguage)
.reduce(
(acc, current) => ({
...acc,
[current[0]]: current[1],
}),
{},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Translations } from '@/Translations';
import { en } from './locales/en';
import { fr } from './locales/fr';

export const homeTranslations: Translations = { fr, en };
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Translation } from '@/Translations';

export const en: Translation = {
home: {
translationEnabled: 'Internationalization enabled',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Translation } from '@/Translations';

export const fr: Translation = {
home: {
translationEnabled: 'Internationalisation activée',
},
};
15 changes: 15 additions & 0 deletions src/main/resources/generator/client/common/i18n/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { homeTranslations } from './home/HomeTranslations';
import { toTranslationResources } from './Translations';

i18n.use(LanguageDetector).init({
fallbackLng: 'en',
debug: false,
interpolation: {
escapeValue: false,
},
resources: toTranslationResources(homeTranslations),
});

export default i18n;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import HomepageVue from '@/home/infrastructure/primary/HomepageVue.vue';
import { shallowMount, VueWrapper } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';

let wrapper: VueWrapper;

const wrap = () => {
wrapper = shallowMount(HomepageVue);
};

describe('App I18next', () => {
it('should renders with translation', () => {
wrap();

expect(wrapper.text()).toContain('translationEnabled');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { config } from '@vue/test-utils';

config.global.mocks = {
$t: msg => msg,
};
1 change: 1 addition & 0 deletions src/main/resources/generator/dependencies/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "4.0.2",
"piqure": "2.0.0",
Expand Down
19 changes: 19 additions & 0 deletions src/test/features/client/vue-i18n.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Feature: Vue i18n

Scenario: Should apply vue i18n module to vue
When I apply modules to default project
| init |
| prettier |
| typescript |
| 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/home/"
| HomeTranslations.ts |
And I should have files in "src/main/webapp/app/home/locales"
| fr.ts |
| en.ts |
And I should have files in "src/test"
| setupTests.ts |
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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")
.and()
.hasFile("src/main/webapp/app/Translations.ts")
.and()
.hasFile("src/main/webapp/app/home/HomeTranslations.ts")
.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/home/infrastructure/primary/HomepageVue.vue")
.containing("<h2 v-html=\"$t('home.translationEnabled')\"></h2>")
.and()
.hasFile("src/test/setupTests.ts")
.and()
.hasFile("vitest.config.ts")
.containing("setupFiles: ['./src/test/setupTests.ts']")
.and()
.hasFile("src/main/webapp/app/home/locales/en.ts")
.and()
.hasFile("src/main/webapp/app/home/locales/fr.ts")
.and()
.hasFile("src/test/webapp/unit/home/infrastructure/primary/HomepageVue.spec.ts");
}

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/HomepageVue.vue.template",
"src/main/webapp/app/home/infrastructure/primary/HomepageVue.vue"
);
}

private ModuleFile homepageTest() {
return file(
"src/test/resources/projects/vue/HomepageVue.spec.ts.template",
"src/test/webapp/unit/home/infrastructure/primary/HomepageVue.spec.ts"
);
}

private ModuleFile vitest() {
return file("src/test/resources/projects/vue/vitest.config.ts.template", "./vitest.config.ts");
}
}
17 changes: 17 additions & 0 deletions src/test/resources/projects/vue/HomepageVue.spec.ts.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import HomepageVue from '@/home/infrastructure/primary/HomepageVue.vue';
import { shallowMount, VueWrapper } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';

let wrapper: VueWrapper;

const wrap = () => {
wrapper = shallowMount(HomepageVue);
};

describe('App I18next', () => {
it('should renders with translation', () => {
wrap();

expect(wrapper.text()).toContain('translationEnabled');
});
});
Loading

0 comments on commit 41cd23f

Please sign in to comment.