diff --git a/.github/workflows/maven-test.yml b/.github/workflows/maven-test.yml new file mode 100644 index 0000000..b4404f3 --- /dev/null +++ b/.github/workflows/maven-test.yml @@ -0,0 +1,28 @@ +name: "Maven Test" +run-name: Maven Test [${{ github.ref_type }}][${{ github.ref }}] +on: + push: + branches: + - main + - stage + - dev +# workflow_dispatch: +jobs: + tests: + name: Run tests + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-package: 'jdk' + java-version: '17' + check-latest: true + cache: 'maven' + - name: Install & Run Rests + run: | + mvn install + mvn test diff --git a/.github/workflows/ossrh-release.yml b/.github/workflows/ossrh-release.yml new file mode 100644 index 0000000..5dd1042 --- /dev/null +++ b/.github/workflows/ossrh-release.yml @@ -0,0 +1,35 @@ +# Action configuration is based on the following articles: +# https://blogs.itemis.com/en/github-actions-releasing-artifacts-into-maven-central +name: "OSSRH Release" +run-name: OSSRH Release [${{ github.ref_type }}][${{ github.ref }}] +on: + workflow_dispatch: +jobs: + release: + name: Release + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-package: 'jdk' + java-version: '17' + check-latest: true + server-id: 'ossrh' + server-username: OSSRH_USERNAME + server-password: OSSRH_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PRIVATE_KEY_PASSPHRASE + cache: 'maven' + - name: Build & Deploy + run: | + mvn -U -B clean deploy -P release + env: + GPG_PRIVATE_KEY_PASSPHRASE: ${{ secrets.GPG_PRIVATE_KEY_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..2db72e9 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,57 @@ +name: "SonarCloud Analyze" +run-name: SonarCloud Analyze [${{ github.ref_type }}][${{ github.ref }}] +on: + # push: + # branches: + # - master + # pull_request: + # types: [opened, synchronize, reopened] + workflow_dispatch: +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }} # -Dsonar.projectKey=$SONAR_PROJECTKEY + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get branch name + id: branch-name + uses: tj-actions/branch-names@v7 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'zulu' + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build and analyze (dev) + run: | + mvn test -P test-coverage + mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ + -Dsonar.projectName=${{ vars.SONAR_PROJECT_NAME }} \ + -Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }} \ + -Dsonar.branch.name=${{ steps.branch-name.outputs.ref_branch }} \ + -Dsonar.organization=${{ vars.SONAR_ORGANIZATION }} \ + -Dsonar.host.url=${{ vars.SONAR_HOST_URL }} \ + -Dsonar.coverage.jacoco.xmlReportPaths=${{ vars.SONAR_COVERAGE_JACOCO_XML_REPORT_PATHS }} \ + -Dsonar.sources=${{ vars.SONAR_SOURCES }} \ + -Dsonar.tests=${{ vars.SONAR_TESTS }} \ + -Dsonar.exclusions=${{ vars.SONAR_EXCLUSIONS }} \ + -Dsonar.java.binaries=${{ vars.SONAR_JAVA_BINARIES }} \ + -Dsonar.coverage.exclusions=${{ vars.SONAR_COVERAGE_EXCLUSIONS }} \ + -Dsonar.java.source=${{ vars.SONAR_JAVA_SOURCE }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d491469 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +HELP.md +target/ +!**/src/main/**/target/ +!**/src/test/**/target/ +mvnw +mvnw.cmd +/.mvn/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fa477e --- /dev/null +++ b/README.md @@ -0,0 +1,409 @@ +# XLIFF translation support for Spring Boot and Spring + +This package provides a **MessageSource** for using translations from XLIFF files. The package support XLIFF versions 1.2, 2.0 and 2.1. + +**Table of content** + +1. [Version](#Versions) +2. [Dependency](#Dependency) +3. [MessageSource Configuration](#MessageSource-Configuration) +4. [Minimal CacheManager Configuration](#Minimal-CacheManager-configuration) +5. [CacheManager with supported Cache Providers](#CacheManager-with-supported-Cache-Providers) +6. [Cache warming with an ApplicationRunner (recommended)](#Cache-warming-with-an-ApplicationRunner-recommended) +7. [Xliff Translations files](#Xliff-Translations-files) +8. [Example with Translations files](#Example-with-Translations-files) +9. [Full Example](#Full-Example) +10. [Support](#Support) +## Versions + +| Version | Description | +|:--------|:--------------------------------------------| +| 1.0.0 | First public version | + + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=alaugks_spring-xliff-translation&metric=alert_status)](https://sonarcloud.io/summary/overall?id=alaugks_spring-xliff-translation) [![Maven Central](https://img.shields.io/maven-central/v/io.github.alaugks/spring-messagesource-xliff.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.alaugks/spring-messagesource-xliff/1.0.0) + +## Dependency + +**Maven** +```xml + + io.github.alaugks + spring-messagesource-xliff + 1.0.0 + +``` + +**Gradle** +```text +implementation group: 'io.github.alaugks', name: 'spring-messagesource-xliff', version: '1.0.0' +``` + +## MessageSource Configuration + +The class XliffTranslationMessageSource implements the [MessageSource](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/MessageSource.html) interface. An instance of the [CacheManager](https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-caching.html#boot-features-caching-provider) is required for caching the translations. + +### XliffTranslationMessageSource + +`setBasenamePattern(String basename)` or `setBasenamesPattern(Iterable basenames)` (*mandatory*) + +* Defines the pattern used to select the XLIFF files. +* The package uses the [PathMatchingResourcePatternResolver](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/PathMatchingResourcePatternResolver.html) to select the XLIFF files. So you can use the supported patterns. +* Files with the extension `xliff` and `xlf` are filtered from the result list. + +`setDefaultLocale(Locale locale)` (*mandatory*) +* Defines the default language. + +`setDefaultDomain(String defaultDomain)` +* Defines the default domain. Default is 'messages'. For more information, see Xliff translations files. + +> Please note the [Minimal CacheManager Configuration](#Minimal-CacheManager-configuration). + +```java +import de.alaugks.spring.XliffTranslationMessageSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Locale; + +@Configuration +public class MessageConfig { + + @Bean("messageSource") + public MessageSource messageSource(CacheManager cacheManager) { + XliffMessageSourcePatternResolver messageSource = new XliffTranslationMessageSource(cacheManager); + messageSource.setDefaultLocale(Locale.forLanguageTag("en")); + messageSource.setBasenamePattern("translations/*"); + return messageSource; + } + +} +``` + +## Minimal CacheManager Configuration + +You may already have an existing CacheManager configuration. If not, the following minimum CacheManager configuration is required. + +The CacheName must be set with the constant `CatalogCache.CACHE_NAME`. The specific cache identifier is stored in the constant. Currently you cannot set a custom cache name. + +[ConcurrentMapCacheManager](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/concurrent/ConcurrentMapCacheManager.html) is the default cache in Spring Boot and Spring. + +### CacheConfig + +```java +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogCache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@EnableCaching +public class CacheConfig { + @Bean + public CacheManager cacheManager() { + ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); + cacheManager.setCacheNames(List.of(CatalogCache.CACHE_NAME)); + return cacheManager; + } +} +``` + +## CacheManager with supported Cache Providers + +A [supported Cache Providers](https://docs.spring.io/spring-boot/docs/3.1.1/reference/html/io.html#io.caching.provider) can also be used. Here is an example using [Caffeine](https://github.com/ben-manes/caffeine): + +### CacheConfig +The CacheName must be set with the constant `CatalogCache.CACHE_NAME`. No ExpireDate should be set for the XLIFF Translations cache. + +```java +import com.github.benmanes.caffeine.cache.Caffeine; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogCache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collection; +import java.util.List; + +@Configuration +@EnableCaching +class CacheConfig { + @Bean + public Caffeine caffeineConfig() { + return Caffeine.newBuilder(); + } + + @Bean + public CacheManager cacheManager(Caffeine caffeine) { + Collection cacheNames = List.of(CatalogCache.CACHE_NAME); + CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); + caffeineCacheManager.setCaffeine(caffeine); + caffeineCacheManager.setCacheNames(cacheNames); + return caffeineCacheManager; + } +} +``` + +## Cache warming with an ApplicationRunner (recommended) + +In the following example, the cache of translations is warmed up after the application starts. + +```java +import io.github.alaugks.spring.messagesource.xliff.XliffMessageSourcePatternResolver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +@Component +public class AppStartupRunner implements ApplicationRunner { + + @Autowired + MessageSource messageSource; + + @Override + public void run(ApplicationArguments args) { + if (this.messageSource instanceof XliffTranslationMessageSource) { + ((XliffTranslationMessageSource) this.messageSource).initCache(); + } + } +} +``` + +## XLIFF Translations files + +* Translations can be separated into different files (domains). The default domain is `messages`. +* The default domain can be defined. +* Translation files must be stored in the resource folder and have the extension `xliff` or `xlf`. +* For performance reasons, there is no validation of XLIFF files against an xml schema. If there is any broken XML in an XLIFF file, the SAX parser will throw a [Fatal Error]. + +### Structure of the translation filename + +``` +# Default language +.xlf + +# Domain + Language +[-_].xlf + +# Domain + Language + Region +[-_][-_].xlf +``` + +## Example with Translations files + +* Default domain is `messages`. +* Default locale is `en` without region. +* Translations are provided for the locale `de` (without region) and `en-US`. + +``` +[resources] + |-[translations] + |-messages.xliff // Default domain and default language + |-messages_de.xliff + |-messages_en-US.xliff + |-payment.xliff // Default language + |-payment_de.xliff + |-payment_en-US.xliff +``` + +### Translations files + +**messages.xliff** +```xml + + + + + + Headline + Headline + + + Postcode + Postcode + + + + +``` + +**messages_de.xliff** +```xml + + + + + + Headline + Überschrift + + + Postcode + Postleitzahl + + + + +``` + +**messages_en-US.xliff** +```xml + + + + + + Postcode + Zip code + + + + +``` + +**payment.xliff** +```xml + + + + + + Payment + Payment + + + Expiry date + Expiry date + + + + +``` + +**payment_de.xliff** +```xml + + + + + + Payment + Zahlung + + + Expiry date + Ablaufdatum + + + + +``` + +**payment_en-US.xliff** +```xml + + + + + + Payment + Payment + + + Expiry date + Expiration date + + + + +``` + +**Output translation** + +| id | en | de | en-US | +|---------------------|-------------|--------------|-----------------| +| postcode* | Postcode | Postleitzahl | Zip code | +| messages.postcode | Postcode | Postleitzahl | Zip code | +| headline* | Headline | Überschrift | Headline** | +| messages.headline | Headline | Überschrift | Headline** | +| payment.headline | Payment | Zahlung | Payment | +| payment.expiry_date | Expiry date | Ablaufdatum | Expiration date | + +> *Default domain is `messages`. +> +> **Example of a fallback. With locale `en-US` it tries to select the translation with id `headline` in messages_en-US. The id `headline` does not exist, so it tries to select the translation with locale `en` in messages. + + + +## Full Example + +A complete example using Spring Boot, including the use of XLIFF 1.2 and XLIFF 2.1 translation files: https://github.com/alaugks/spring-xliff-translation-example-spring-boot + +## Support + +If you have any questions, comments or feature requests, please use the [Discussions](https://github.com/alaugks/spring-xliff-translation/discussions) section to contact me. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1b1017c --- /dev/null +++ b/pom.xml @@ -0,0 +1,278 @@ + + + 4.0.0 + + io.github.alaugks + spring-messagesource-xliff + 1.0.0 + jar + + ${project.groupId}:${project.artifactId} + Provides a MessageSource for translations from XLIFF files for Spring and Spring Boot. + https://github.com/alaugks/spring-xliff-translation + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + André Laugks + package@pb82.de + + + + + scm:git:https://github.com/alaugks/spring-xliff-translation + scm:git:https://github.com/alaugks/spring-xliff-translation + https://github.com/alaugks/spring-xliff-translation + + + + org.springframework.boot + spring-boot-starter-parent + 3.1.1 + + + + 11 + 5.9.3 + + + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + + + + + + + + local + + + central + file:~/.m2/ + + + + + + + + + + release + + 3.0.0-M7 + 3.0.1 + 1.6.13 + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + + maven-release-plugin + ${version.maven-release-plugin} + + @{project.version} + + + + maven-gpg-plugin + ${version.maven-gpg-plugin} + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${version.nexus-staging-maven-plugin} + true + + ossrh + https://s01.oss.sonatype.org + true + + + + + + + maven-gpg-plugin + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + + + + + + + + test-coverage + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + --illegal-access=permit + + true + 2 + true + ${surefireArgLine} + + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + default-prepare-agent + + prepare-agent + + + ${project.build.directory}/coverage-reports/jacoco.exec + surefireArgLine + + + + default-report + test + + report + + + ${project.build.directory}/coverage-reports/jacoco.exec + ${project.reporting.outputDirectory}/jacoco + + + + default-check + + check + + + + + BUNDLE + + + COMPLEXITY + COVEREDRATIO + 0.70 + + + + + + + + + + + + + + diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/XliffCacheableKeyGenerator.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/XliffCacheableKeyGenerator.java new file mode 100644 index 0000000..4c38936 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/XliffCacheableKeyGenerator.java @@ -0,0 +1,23 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogUtilities; +import org.springframework.cache.interceptor.KeyGenerator; + +import java.lang.reflect.Method; +import java.util.Locale; + +public final class XliffCacheableKeyGenerator implements KeyGenerator { + + public static final String GENERATOR_NAME = "messagesource.xliff.KEY_GENERATOR"; + + public static String createCode(Locale locale, String code) { + return CatalogUtilities.localeToKey(locale) + "|" + code; + } + + public Object generate(Object target, Method method, Object... params) { + return createCode( + (Locale) params[2], + params[0].toString() + ); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/XliffTranslationMessageSource.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/XliffTranslationMessageSource.java new file mode 100644 index 0000000..fa99129 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/XliffTranslationMessageSource.java @@ -0,0 +1,115 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.Catalog; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogWrapper; +import io.github.alaugks.spring.messagesource.xliff.catalog.xliff.XliffCatalogBuilder; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoader; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoaderInterface; +import org.springframework.cache.CacheManager; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.lang.Nullable; + +import java.text.MessageFormat; +import java.util.Locale; + +public class XliffTranslationMessageSource implements MessageSource { + + private final CatalogWrapper catalogWrapper; + private final ResourcesLoaderInterface resourcesLoader = new ResourcesLoader(); + + public XliffTranslationMessageSource(CacheManager cacheManager) { + this.catalogWrapper = new CatalogWrapper( + cacheManager, + this.resourcesLoader, + new XliffCatalogBuilder(), + new Catalog() + ); + } + + public XliffTranslationMessageSource setDefaultLocale(Locale locale) { + this.resourcesLoader.setDefaultLocale(locale); + return this; + } + + public XliffTranslationMessageSource setBasenamePattern(String basename) { + this.resourcesLoader.setBasenamePattern(basename); + return this; + } + + public XliffTranslationMessageSource setBasenamesPattern(Iterable basenames) { + this.resourcesLoader.setBasenamesPattern(basenames); + return this; + } + + public XliffTranslationMessageSource setDefaultDomain(String defaultDomain) { + this.catalogWrapper.setDefaultDomain(defaultDomain); + return this; + } + + @Nullable + public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + return this.format( + this.internalMessageWithDefaultMessage(code, defaultMessage, locale), + args + ); + } + + public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException { + CatalogWrapper.Translation translation = this.internalMessage(code, locale); + if (translation.exists()) { + return this.format(translation.toString(), args); + } + + throw new NoSuchMessageException(code, locale); + } + + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + String[] codes = resolvable.getCodes(); + if (codes != null) { + for (String code : codes) { + CatalogWrapper.Translation translation = internalMessage(code, locale); + if (translation.exists()) { + return this.format(translation.toString(), resolvable.getArguments()); + } + } + } + if (resolvable instanceof DefaultMessageSourceResolvable) { + String defaultMessage = resolvable.getDefaultMessage(); + if (defaultMessage != null) { + return this.format(defaultMessage, resolvable.getArguments()); + } + } + + throw new NoSuchMessageException(codes != null && codes.length > 0 ? codes[codes.length - 1] : "", locale); + } + + private CatalogWrapper.Translation internalMessage(String code, Locale locale) throws NoSuchMessageException { + return this.findInCatalog(locale, code); + } + + private String internalMessageWithDefaultMessage(String code, @Nullable String defaultMessage, Locale locale) { + CatalogWrapper.Translation translation = this.findInCatalog(locale, code); + if (translation.exists()) { + return translation.toString(); + } + return defaultMessage; + } + + private CatalogWrapper.Translation findInCatalog(Locale locale, String code) { + return this.catalogWrapper.get(locale, code); + } + + public void initCache() { + this.catalogWrapper.initCache(); + } + + private String format(@Nullable String message, @Nullable Object[] args) { + if (message != null && args != null && args.length > 0) { + return new MessageFormat(message).format(args); + } + return message; + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/Catalog.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/Catalog.java new file mode 100644 index 0000000..981ad61 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/Catalog.java @@ -0,0 +1,63 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import java.util.HashMap; +import java.util.Locale; + +public final class Catalog implements CatalogInterface { + + private final HashMap> catalogMap; + + public Catalog() { + this.catalogMap = new HashMap<>(); + } + + @Override + public HashMap> getAll() { + return this.catalogMap; + } + + @Override + public String get(Locale locale, String code) { + if (this.localeExists(locale)) { + HashMap languageCatalog = this.getLocaleHashMap(locale); + if (languageCatalog.containsKey(code)) { + return languageCatalog.get(code); + } + } + return null; + } + + public boolean has(Locale locale, String code) { + return this.get(locale, code) != null; + } + + @Override + public void put(Locale locale, String domain, String code, String targetValue) { + if (locale.toString().length() > 0) { + String concatCode = CatalogUtilities.contactCode(domain, code); + if (this.localeExists(locale)) { + HashMap transUnit = this.getLocaleHashMap(locale); + if (!transUnit.containsKey(concatCode)) { + this.getLocaleHashMap(locale).put(concatCode, targetValue); + } + return; + } + // Init catalog for locale + HashMap transUnit = new HashMap<>(); + transUnit.put(concatCode, targetValue); + this.catalogMap.put(CatalogUtilities.localeToKey(locale), transUnit); + } + } + + public boolean localeExists(Locale locale) { + if (locale.toString().length() > 0) { + return this.catalogMap.containsKey(CatalogUtilities.localeToKey(locale)); + } + return false; + } + + + private HashMap getLocaleHashMap(Locale locale) { + return this.catalogMap.get(CatalogUtilities.localeToKey(locale)); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogBuilderInterface.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogBuilderInterface.java new file mode 100644 index 0000000..ebb642c --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogBuilderInterface.java @@ -0,0 +1,7 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoaderInterface; + +public interface CatalogBuilderInterface { + CatalogInterface createCatalog(ResourcesLoaderInterface resourceLoader, CatalogInterface catalog); +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogCache.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogCache.java new file mode 100644 index 0000000..6344721 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogCache.java @@ -0,0 +1,96 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import io.github.alaugks.spring.messagesource.xliff.XliffCacheableKeyGenerator; +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceCacheNotExistsException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Objects; + +public final class CatalogCache implements CatalogInterface { + public static final String CACHE_NAME = "messagesource.xliff.catalog.CACHE"; + + private static final Logger logger = LogManager.getLogger(); + private Cache cache; + + CatalogCache(CacheManager cacheManager) { + this.loadCache(cacheManager); + } + + private void loadCache(CacheManager cacheManager) { + if (cacheManager != null) { + Collection caches = cacheManager.getCacheNames(); + if (caches.contains(CACHE_NAME)) { + this.cache = cacheManager.getCache(CACHE_NAME); + return; + } + throw new XliffMessageSourceCacheNotExistsException( + String.format("Cache with name [%s] not available.", CACHE_NAME) + ); + } + throw new XliffMessageSourceCacheNotExistsException( + "org.springframework.cache.CacheManager not available." + ); + } + + @Override + public HashMap> getAll() { + return new HashMap<>(); + } + + @Override + public boolean has(Locale locale, String code) { + return this.get(locale, code) != null; + } + + @Override + public String get(Locale locale, String code) { + if (locale.toString().length() > 0) { + return this.getValue( + this.cache.get( + XliffCacheableKeyGenerator.createCode(locale, code) + ) + ); + } + return null; + } + + @Override + public void put(Locale locale, String domain, String code, String targetValue) { + this.put(locale, CatalogUtilities.contactCode(domain, code), targetValue); + } + + void put(Locale locale, String code, String targetValue) { + if (locale.toString().length() > 0) { + this.cache.put( + XliffCacheableKeyGenerator.createCode(locale, code), + targetValue + ); + } + } + + private String getValue(Cache.ValueWrapper valueWrapper) { + if (valueWrapper != null) { + return Objects.requireNonNull(valueWrapper.get()).toString(); + } + return null; + } + + void initCache(CatalogInterface catalog) { + if (catalog != null) { + logger.info("Init xliff catalog cache"); + catalog.getAll().forEach((langCode, domain) -> domain.forEach((code, targetValue) -> + this.put( + Locale.forLanguageTag(langCode.replace("_", "-")), + code, + targetValue + ) + )); + } + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogInterface.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogInterface.java new file mode 100644 index 0000000..dd70a72 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogInterface.java @@ -0,0 +1,15 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import java.util.HashMap; +import java.util.Locale; + +public interface CatalogInterface { + // HashMap<"language+region", HashMap<"code", "targetValue">> + HashMap> getAll(); + + String get(Locale locale, String code); + + void put(Locale locale, String domain, String code, String targetValue); + + boolean has(Locale locale, String code); +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogUtilities.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogUtilities.java new file mode 100644 index 0000000..6380a86 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogUtilities.java @@ -0,0 +1,39 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import java.util.Locale; + +public class CatalogUtilities { + + private CatalogUtilities() { + throw new IllegalStateException("CatalogUtilities class"); + } + + public static String localeToKey(Locale locale) { + return buildLocale(locale).toString().trim().toLowerCase().replace("_", "-"); + } + + public static String contactCode(String domain, String code) { + return domain + "." + code; + } + + public static Locale buildLocale(Locale locale) { + Locale.Builder localeBuilder = new Locale.Builder(); + localeBuilder.setLanguage(locale.getLanguage()); + if (locale.getCountry().length() > 0) { + localeBuilder.setRegion(locale.getCountry()); + } + return localeBuilder.build(); + } + + public static Locale buildLocale(String language, String region) { + Locale.Builder localeBuilder = new Locale.Builder(); + if (language != null && language.length() > 0) { + localeBuilder.setLanguage(language); + // Set region only language is present + if (region != null && region.length() > 0) { + localeBuilder.setRegion(region); + } + } + return localeBuilder.build(); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogWrapper.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogWrapper.java new file mode 100644 index 0000000..c0b92c0 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogWrapper.java @@ -0,0 +1,161 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoaderInterface; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.cache.CacheManager; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Objects; + +public final class CatalogWrapper { + private static final Logger logger = LogManager.getLogger(); + private final CatalogCache catalogCache; + private final CatalogInterface catalog; + private final CatalogBuilderInterface catalogBuilder; + private final ResourcesLoaderInterface resourcesLoader; + private String defaultDomain = "messages"; + + public CatalogWrapper(CacheManager cacheManager, + ResourcesLoaderInterface resourcesLoader, + CatalogBuilderInterface catalogBuilder, + CatalogInterface catalog + ) { + this.catalog = catalog; + this.catalogBuilder = catalogBuilder; + this.resourcesLoader = resourcesLoader; + this.catalogCache = new CatalogCache(cacheManager); + } + + private static Locale buildLocaleWithLanguageRegion(Locale locale) { + return CatalogUtilities.buildLocale(locale); + } + + private static Locale buildLocaleWithLanguage(Locale locale) { + Locale.Builder localeBuilder = new Locale.Builder(); + localeBuilder.setLanguage(locale.getLanguage()); + return localeBuilder.build(); + } + + public Translation get(Locale locale, String code) { + // Check cache + String targetValue = this.getByChain( + this.catalogCache, + locale, + code + ); + + // Exists in cache? + if (targetValue != null) { + return new Translation(code, targetValue); + } + + // Check catalog + CatalogInterface loadedCatalog = this.loadCatalog(); + targetValue = this.getByChain( + loadedCatalog, + locale, + code + ); + + // If exists then init cache, because it was not in the cache. Cache empty? + if (targetValue != null) { + logger.info("Re-init xliff catalog cache"); + this.catalogCache.initCache(loadedCatalog); + } + + // If not exists then is the targetValue is the code. + // + // Non-existing code is added to the cache to fetch the non-existing code from + // the cache so as not to continue looking in the messagesource files. + String targetValueCode = null; + if (targetValue == null) { + targetValueCode = code; + this.put(locale, this.defaultDomain, code, targetValueCode); + } + + return new Translation(code, (targetValue != null ? targetValue : targetValueCode)); + } + + void put(Locale locale, String domain, String code, String targetValue) { + this.catalogCache.put( + locale, + CatalogUtilities.contactCode(domain, code), + targetValue + ); + } + + public void setDefaultDomain(String defaultDomain) { + this.defaultDomain = defaultDomain; + } + + public void initCache() { + // Without cache do not get load catalog + this.catalogCache.initCache(this.loadCatalog()); + } + + private CatalogInterface loadCatalog() { + return this.catalogBuilder.createCatalog(this.resourcesLoader, this.catalog); + } + + private String chainLink(CatalogInterface catalog, Locale locale, String code) { + String targetValue; + HashMap locales = new HashMap<>(); + // Follow the order + locales.put(0, locale); // First + locales.put(1, this.resourcesLoader.getDefaultLocale()); // Second + for (int i = 0; i < locales.size(); i++) { + if(locales.containsKey(i) && locales.get(i) != null) { + // Try with locale+region + targetValue = catalog.get( + buildLocaleWithLanguageRegion(locales.get(i)), + code + ); + + // Try with locale + if (targetValue == null) { + targetValue = catalog.get( + buildLocaleWithLanguage(locales.get(i)), + code + ); + } + + if (targetValue != null) { + return targetValue; + } + } + } + return null; + } + + private String getByChain(CatalogInterface catalog, Locale locale, String code) { + String targetValue = this.chainLink(catalog, locale, code); + if (targetValue == null) { + targetValue = this.chainLink( + catalog, + locale, + CatalogUtilities.contactCode(this.defaultDomain, code) + ); + } + return targetValue; + } + + public static class Translation { + String code; + String targetValue; + + public Translation(String code, String targetValue) { + this.code = code; + this.targetValue = targetValue; + } + + public boolean exists() { + return !Objects.equals(this.code, this.targetValue); + } + + public String toString() { + return targetValue; + } + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff12.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff12.java new file mode 100644 index 0000000..0205e18 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff12.java @@ -0,0 +1,22 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import java.util.Locale; + +final class Xliff12 extends XliffAbstract implements XliffInterface { + static final String VERSION = "1.2"; + + @Override + public boolean support(String version) { + return version.equals(VERSION); + } + + @Override + public void read(CatalogInterface catalog, Document document, String domain, Locale locale) { + NodeList translationNodes = XliffParserUtility.getTranslationNodes(document, "trans-unit"); + this.readItems(VERSION, catalog, domain, locale, translationNodes); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff20.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff20.java new file mode 100644 index 0000000..4eb32c8 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff20.java @@ -0,0 +1,22 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import java.util.Locale; + +final class Xliff20 extends XliffAbstract implements XliffInterface { + static final String VERSION = "2.0"; + + @Override + public boolean support(String version) { + return version.equals(VERSION); + } + + @Override + public void read(CatalogInterface catalog, Document document, String domain, Locale locale) { + NodeList translationNodes = XliffParserUtility.getTranslationNodes(document, "segment"); + this.readItems(VERSION, catalog, domain, locale, translationNodes); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff21.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff21.java new file mode 100644 index 0000000..b4d615b --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff21.java @@ -0,0 +1,20 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.w3c.dom.Document; + +import java.util.Locale; + +final class Xliff21 implements XliffInterface { + static final String VERSION = "2.1"; + + @Override + public boolean support(String version) { + return version.equals(VERSION); + } + + @Override + public void read(CatalogInterface catalog, Document document, String domain, Locale locale) { + new Xliff20().read(catalog, document, domain, locale); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffAbstract.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffAbstract.java new file mode 100644 index 0000000..9497fcb --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffAbstract.java @@ -0,0 +1,45 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Locale; + +// http://docs.oasis-open.org/xliff/xliff-core/v2.0/csprd01/xliff-core-v2.0-csprd01.html#segment +// http://docs.oasis-open.org/xliff/xliff-core/v2.1/os/xliff-core-v2.1-os.html#segment +// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html#General_Identifiers +public class XliffAbstract { + protected void readItems( + String version, + CatalogInterface catalog, + String domain, + Locale locale, + NodeList translationNodes + ) { + for (int item = 0; item < translationNodes.getLength(); item++) { + Node translationNode = translationNodes.item(item); + + /* */ + String id = XliffParserUtility.getId(translationNode); + + String resname = null; + if (Xliff12.VERSION.equals(version)) { + resname = XliffParserUtility.getResname(translationNode, "resname"); + } + + /* */ + Element translationNodeElement = (Element) translationNode; + /* */ + String sourceValue = XliffParserUtility.getSource(translationNodeElement); + /* */ + String targetValue = XliffParserUtility.getTargetValue(translationNodeElement); + /* code */ + String code = XliffParserUtility.getCode(id, resname, sourceValue); + + /* Add catalog */ + catalog.put(locale, domain, code, targetValue); + } + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffCatalogBuilder.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffCatalogBuilder.java new file mode 100644 index 0000000..9823bb8 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffCatalogBuilder.java @@ -0,0 +1,70 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogBuilderInterface; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceRuntimeException; +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceVersionSupportException; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoader; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoaderInterface; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.util.ArrayList; + +public final class XliffCatalogBuilder implements CatalogBuilderInterface { + + private CatalogInterface catalog; + + @Override + public CatalogInterface createCatalog(ResourcesLoaderInterface resourceLoader, CatalogInterface catalog) { + try { + this.catalog = catalog; + this.readFile(resourceLoader.getResourcesInputStream()); + return this.catalog; + } catch (ParserConfigurationException | IOException e) { + throw new XliffMessageSourceRuntimeException(e); + } + } + + private void readFile(ArrayList translationFiles) throws ParserConfigurationException, IOException { + + XliffReader xliffReader = new XliffReader(); + + for (ResourcesLoader.Dto translationFile : translationFiles) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document; + try { + document = builder.parse(translationFile.getInputStream()); + } catch (SAXException e) { + throw new XliffMessageSourceRuntimeException(e); + } + + Element root = document.getDocumentElement(); + + // Simple test: Filter if root element not + if (!root.getNodeName().equals("xliff")) { + continue; + } + + String version = XliffParserUtility.getAttributeValue( + root.getAttributes().getNamedItem("version") + ); + + XliffInterface xliffInterface = xliffReader.getReader(version); + if (xliffInterface != null) { + xliffInterface.read(this.catalog, document, translationFile.getDomain(), translationFile.getLocale()); + } else { + throw new XliffMessageSourceVersionSupportException(String.format("XLIFF version \"%s\" not supported.", version)); + } + } + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffInterface.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffInterface.java new file mode 100644 index 0000000..66a5497 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffInterface.java @@ -0,0 +1,12 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.w3c.dom.Document; + +import java.util.Locale; + +public interface XliffInterface { + boolean support(String version); + + void read(CatalogInterface catalog, Document document, String domain, Locale locale); +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffParserUtility.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffParserUtility.java new file mode 100644 index 0000000..2cf2b38 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffParserUtility.java @@ -0,0 +1,79 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import org.w3c.dom.CharacterData; +import org.w3c.dom.*; + +final class XliffParserUtility { + + private XliffParserUtility() { + throw new IllegalStateException("XliffParserUtility class"); + } + + static Node getFirstChild(NodeList nodeList) { + return nodeList.item(0).getFirstChild(); + } + + public static String getAttributeValue(Node node) { + if (node != null) { + return node.getNodeValue(); + } + return null; + } + + private static String getCharacterDataFromElement(Node child) { + if (child instanceof CharacterData) { + if (child.getNextSibling() != null) { + return child.getNextSibling().getTextContent().trim(); + } + return ((CharacterData) child).getData().trim(); + } + return null; + } + + public static String getAttributeValue(Node translationNode, String attributeName) { + return getAttributeValue( + translationNode.getAttributes().getNamedItem(attributeName) + ); + } + + public static String getElementValue(Element translationNodeElement, String elementName) { + Node sourceElement = getFirstChild( + translationNodeElement.getElementsByTagName(elementName) + ); + return getCharacterDataFromElement(sourceElement); + } + + public static String getSource(Element translationNodeElement) { + return getElementValue(translationNodeElement, "source"); + } + + public static String getTargetValue(Element translationNodeElement) { + return getElementValue(translationNodeElement, "target"); + } + + public static NodeList getTranslationNodes(Document document, String nodeName) { + return document.getElementsByTagName(nodeName); + } + + public static String getCode(String id, String resname, String sourceValue) { + // resname|name -> id -> sourceValue + + if (resname != null) { + return resname; + } + + if (id != null) { + return id; + } + + return sourceValue; + } + + public static String getResname(Node translationNode, String name) { + return getAttributeValue(translationNode, name); + } + + public static String getId(Node translationNode) { + return getAttributeValue(translationNode, "id"); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffReader.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffReader.java new file mode 100644 index 0000000..e77728b --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffReader.java @@ -0,0 +1,24 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import java.util.Set; + +final class XliffReader { + Set supportedVersions; + + public XliffReader() { + this.supportedVersions = Set.of( + new Xliff12(), + new Xliff20(), + new Xliff21() + ); + } + + public XliffInterface getReader(String version) { + for (XliffInterface xliffClass : this.supportedVersions) { + if (xliffClass.support(version)) { + return xliffClass; + } + } + return null; + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceCacheNotExistsException.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceCacheNotExistsException.java new file mode 100644 index 0000000..ea205bb --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceCacheNotExistsException.java @@ -0,0 +1,7 @@ +package io.github.alaugks.spring.messagesource.xliff.exception; + +public class XliffMessageSourceCacheNotExistsException extends RuntimeException { + public XliffMessageSourceCacheNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceRuntimeException.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceRuntimeException.java new file mode 100644 index 0000000..3f4ea5b --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceRuntimeException.java @@ -0,0 +1,11 @@ +package io.github.alaugks.spring.messagesource.xliff.exception; + +public class XliffMessageSourceRuntimeException extends RuntimeException { + public XliffMessageSourceRuntimeException(Throwable cause) { + super(cause); + } + + public XliffMessageSourceRuntimeException(String message) { + super(message); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceVersionSupportException.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceVersionSupportException.java new file mode 100644 index 0000000..d9d01b8 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/exception/XliffMessageSourceVersionSupportException.java @@ -0,0 +1,7 @@ +package io.github.alaugks.spring.messagesource.xliff.exception; + +public class XliffMessageSourceVersionSupportException extends RuntimeException { + public XliffMessageSourceVersionSupportException(String message) { + super(message); + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesFileNameParser.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesFileNameParser.java new file mode 100644 index 0000000..a6bf299 --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesFileNameParser.java @@ -0,0 +1,86 @@ +package io.github.alaugks.spring.messagesource.xliff.ressources; + +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogUtilities; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.IllformedLocaleException; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class ResourcesFileNameParser { + private static final Logger logger = LogManager.getLogger(); + private final String filename; + + public ResourcesFileNameParser(String filename) { + this.filename = filename; + } + + public Dto parse() { + String regexp = "^(?[a-z0-9]+)(?:([_-](?[a-z]+))(?:[_-](?[a-z]+))?)?"; + Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(this.filename); + + if (matcher.find()) { + String domain = getGroup(matcher, "domain"); + String language = getGroup(matcher, "language"); + String region = getGroup(matcher, "region"); + return new Dto( + domain, + language, + region + ); + } + return new Dto(null, null, null); + } + + public static class Dto { + private final String domain; + private final String language; + private final String region; + + public Dto(String domain, String lang, String country) { + this.domain = domain; + this.language = lang; + this.region = country; + } + + public String getDomain() { + return domain; + } + + public boolean hasDomain() { + return this.getDomain() != null; + } + + public String getLanguage() { + return language; + } + + public String getRegion() { + return region; + } + + public boolean hasLocale() { + return getLocale() != null && !getLocale().toString().isEmpty(); + } + + public Locale getLocale() { + try { + return CatalogUtilities.buildLocale(this.language, this.region); + } catch (IllformedLocaleException e) { + logger.info(e.getMessage()); + return null; + } + } + } + + private static String getGroup(Matcher matcher, String groupName) { + try { + return matcher.group(groupName); + } catch (IllegalArgumentException | IllegalStateException e) { + return null; + } + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoader.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoader.java new file mode 100644 index 0000000..f4e628a --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoader.java @@ -0,0 +1,127 @@ +package io.github.alaugks.spring.messagesource.xliff.ressources; + +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceRuntimeException; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public final class ResourcesLoader implements ResourcesLoaderInterface { + + private final Set basenameSet = new LinkedHashSet<>(); + private Locale defaultLocale; + private final List fileExtensions = List.of("xlf", "xliff"); + + @Override + public ResourcesLoader setBasenamePattern(String basename) { + this.setBasenamesPattern(List.of(basename)); + return this; + } + + @Override + public ResourcesLoader setBasenamesPattern(Iterable basenames) { + this.basenameSet.clear(); + this.addBasenames(basenames); + return this; + } + + @Override + public ResourcesLoader setDefaultLocale(Locale locale) { + this.defaultLocale = locale; + return this; + } + + @Override + public Locale getDefaultLocale() { + return defaultLocale; + } + + @Override + public ArrayList getResourcesInputStream() throws IOException { + if (this.defaultLocale == null || this.defaultLocale.toString().isEmpty()) { + throw new XliffMessageSourceRuntimeException("Default language is not set or empty."); + } + + ArrayList translationFiles = new ArrayList<>(); + PathMatchingResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver(); + for (String basename : getBasenameSet()) { + Resource[] resources = resourceLoader.getResources(basename); + for (Resource resource : resources) { + if (this.isFileExtensionSupported(resource)) { + Dto dto = this.parseFileName(resource); + if(dto != null) { + translationFiles.add(dto); + } + } + } + } + + return translationFiles; + } + + private Dto parseFileName(Resource resource) throws IOException { + ResourcesFileNameParser.Dto dto = new ResourcesFileNameParser(resource.getFilename()).parse(); + if (dto.hasDomain()) { + return new Dto( + dto.getDomain(), + dto.hasLocale() + ? dto.getLocale() + : this.defaultLocale, + resource.getInputStream() + ); + } + return null; + } + + private boolean isFileExtensionSupported(Resource resource) { + String fileExtension = this.getFileExtension(resource.getFilename()); + return fileExtension != null && this.fileExtensions.contains(fileExtension.toLowerCase()); + } + + public static class Dto { + private final String domain; + private final Locale locale; + private final InputStream inputStream; + + public Dto(String domain, Locale locale, InputStream inputStream) { + this.domain = domain; + this.locale = locale; + this.inputStream = inputStream; + } + + public String getDomain() { + return domain; + } + + public Locale getLocale() { + return locale; + } + + public InputStream getInputStream() { + return inputStream; + } + } + + private String getFileExtension(String filename) { + return Optional.ofNullable(filename) + .filter(f -> f.contains(".")) + .map(f -> f.substring(filename.lastIndexOf(".") + 1)).orElse(null); + } + + private void addBasenames(Iterable basenames) { + if (!ObjectUtils.isEmpty(basenames)) { + for (String basename : basenames) { + Assert.hasText(basename, "Basename must not be empty"); + this.basenameSet.add(basename.trim()); + } + } + } + + private Set getBasenameSet() { + return this.basenameSet; + } +} diff --git a/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoaderInterface.java b/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoaderInterface.java new file mode 100644 index 0000000..fd6a32b --- /dev/null +++ b/src/main/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoaderInterface.java @@ -0,0 +1,17 @@ +package io.github.alaugks.spring.messagesource.xliff.ressources; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +public interface ResourcesLoaderInterface { + ArrayList getResourcesInputStream() throws IOException; + + ResourcesLoader setDefaultLocale(Locale locale); + + Locale getDefaultLocale(); + + ResourcesLoader setBasenamePattern(String basename); + + ResourcesLoader setBasenamesPattern(Iterable basenames); +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/TestUtilities.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/TestUtilities.java new file mode 100644 index 0000000..b81ff68 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/TestUtilities.java @@ -0,0 +1,64 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.Catalog; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogCache; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogWrapper; +import io.github.alaugks.spring.messagesource.xliff.catalog.xliff.XliffCatalogBuilder; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoader; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoaderInterface; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Locale; + +public class TestUtilities { + public static CatalogInterface getTestCatalog() { + ResourcesLoader translationResourcesLoader = new ResourcesLoader(); + translationResourcesLoader.setBasenamePattern("translations/*"); + translationResourcesLoader.setDefaultLocale(Locale.forLanguageTag("en")); + return new XliffCatalogBuilder().createCatalog(translationResourcesLoader, new Catalog()); + } + + public static CatalogWrapper getCacheWrapperWithCachedTestCatalog() { + CatalogWrapper catalogWrapper = new CatalogWrapper( + getMockedCacheManager(), + getResourcesLoader(), new XliffCatalogBuilder(), TestUtilities.getTestCatalog() + ); + catalogWrapper.initCache(); + return catalogWrapper; + } + + public static CacheManager getMockedCacheManager() { + return getMockedCacheManager(CatalogCache.CACHE_NAME); + } + + public static CacheManager getMockedCacheManager(String cacheName) { + ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); + cacheManager.setCacheNames(List.of(cacheName)); + return cacheManager; + } + + public static ResourcesLoaderInterface getResourcesLoader() { + ResourcesLoader resourcesLoader = new ResourcesLoader(); + resourcesLoader.setBasenamePattern( + "translations/*" + ); + resourcesLoader.setDefaultLocale(Locale.forLanguageTag("en")); + return resourcesLoader; + } + + public static Document getDocument(InputStream inputStream) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputStream); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffCacheableKeyGeneratorTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffCacheableKeyGeneratorTest.java new file mode 100644 index 0000000..7cf024e --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffCacheableKeyGeneratorTest.java @@ -0,0 +1,50 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class XliffCacheableKeyGeneratorTest { + + private Object target; + private Method method; + + @BeforeEach + void beforeEach() { + this.target = new Object(); + this.method = getClass().getMethods()[0]; + } + + @Test + void test_createKey_code_en() { + XliffCacheableKeyGenerator generator = new XliffCacheableKeyGenerator(); + + Locale locale = Locale.forLanguageTag("en"); + String code = "my-code"; + Object[] params = {code, "args", locale}; + + assertEquals("en|my-code", XliffCacheableKeyGenerator.createCode(locale, code)); + assertEquals("en|my-code", generator.generate(this.target, this.method, params)); + } + + @Test + void test_createKey_code_enUk() { + XliffCacheableKeyGenerator generator = new XliffCacheableKeyGenerator(); + + Locale locale = Locale.forLanguageTag("en-GB"); + String code = "my-code"; + Object[] params = {code, "args", locale}; + + assertEquals("en-gb|my-code", XliffCacheableKeyGenerator.createCode(locale, code)); + assertEquals("en-gb|my-code", generator.generate(this.target, this.method, params)); + } + + @Test + void test_Constants() { + assertEquals("messagesource.xliff.KEY_GENERATOR", XliffCacheableKeyGenerator.GENERATOR_NAME); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverAbstract.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverAbstract.java new file mode 100644 index 0000000..9b62528 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverAbstract.java @@ -0,0 +1,153 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import org.junit.jupiter.api.Test; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.support.DefaultMessageSourceResolvable; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +abstract class XliffMatchingResourcePatternResolverAbstract { + + public XliffTranslationMessageSource resolver; + + @Test + void test_message_withDefaultMessage_messageExists() { + String message = this.resolver.getMessage( + "hello_language", + null, + "My default message", + Locale.forLanguageTag("en") + ); + + assertEquals("Hello EN (messages)", message); + } + + @Test + void test_getMessage_withDefaultMessage_messageNotExists() { + String message = this.resolver.getMessage( + "not_exists", + null, + "My default message", + Locale.forLanguageTag("en") + ); + + assertEquals("My default message", message); + } + + @Test + void test_getMessage_withDefaultMessage_messageExists_messageWithArgs() { + Object[] args = {"Road Runner", "Wile E. Coyote"}; + String message = this.resolver.getMessage( + "roadrunner", + args, + "My default message", + Locale.forLanguageTag("en") + ); + + assertEquals("Road Runner and Wile E. Coyote", message); + } + + @Test + void test_getMessage_withDefaultMessage_messageNotExists_defaultMessageWithArgs() { + Object[] args = {"Road Runner", "Wile E. Coyote"}; + String message = this.resolver.getMessage( + "not_exists", + args, + "{0} and {1} as default", + Locale.forLanguageTag("en") + ); + + assertEquals("Road Runner and Wile E. Coyote as default", message); + } + + @Test + void test_getMessage_messageExists() { + String message = this.resolver.getMessage( + "hello_language", + null, + Locale.forLanguageTag("en") + ); + + assertEquals("Hello EN (messages)", message); + } + + @Test + void test_getMessage_messageNotExists() { + Locale locale = Locale.forLanguageTag("en"); + + NoSuchMessageException exception = assertThrows(NoSuchMessageException.class, () -> { + this.resolver.getMessage( + "not_exists", + null, + locale + ); + }); + + assertEquals("No message found under code 'not_exists' for locale 'en'.", exception.getMessage()); + } + + @Test + void test_getMessage_messageExists_messageWithArgs() { + Object[] args = {"Road Runner", "Wile E. Coyote"}; + String message = this.resolver.getMessage( + "roadrunner", + args, + Locale.forLanguageTag("en") + ); + + assertEquals("Road Runner and Wile E. Coyote", message); + } + + @Test + void test_getMessage_resolvable_messageExists_messageWithArgs() { + String[] codes = {"roadrunner"}; + Object[] args = {"Road Runner", "Wile E. Coyote"}; + DefaultMessageSourceResolvable resolvable = new DefaultMessageSourceResolvable( + codes, + args + ); + String message = this.resolver.getMessage( + resolvable, + Locale.forLanguageTag("en") + ); + + assertEquals("Road Runner and Wile E. Coyote", message); + } + + @Test + void test_getMessage_resolvable_messageNotExists() { + Locale locale = Locale.forLanguageTag("en"); + String[] codes = {"not_exists"}; + DefaultMessageSourceResolvable resolvable = new DefaultMessageSourceResolvable( + codes + ); + + NoSuchMessageException exception = assertThrows(NoSuchMessageException.class, () -> { + this.resolver.getMessage( + resolvable, + locale + ); + }); + assertEquals("No message found under code 'not_exists' for locale 'en'.", exception.getMessage()); + } + + @Test + void test_getMessage_resolvable_messageNotExists_withDefaultMessage() { + String[] codes = {"not_exists"}; + String defaultMessage = "This is a default message."; + DefaultMessageSourceResolvable resolvable = new DefaultMessageSourceResolvable( + codes, + null, + defaultMessage + ); + String message = this.resolver.getMessage( + resolvable, + Locale.forLanguageTag("en") + ); + + assertEquals(defaultMessage, message); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverCasesTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverCasesTest.java new file mode 100644 index 0000000..2296d17 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverCasesTest.java @@ -0,0 +1,95 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceRuntimeException; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class XliffMatchingResourcePatternResolverCasesTest { + + @Test + void test_withRegion_setDefaultLocale_notSet() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + + XliffMessageSourceRuntimeException exception = assertThrows( + XliffMessageSourceRuntimeException.class, resolver::initCache + ); + assertEquals("Default language is not set or empty.", exception.getMessage()); + } + + @Test + void test_withRegion_setDefaultLocale_empty() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + resolver.setDefaultLocale(Locale.forLanguageTag("")); + + XliffMessageSourceRuntimeException exception = assertThrows( + XliffMessageSourceRuntimeException.class, resolver::initCache + ); + assertEquals("Default language is not set or empty.", exception.getMessage()); + } + + @Test + void test_withRegion_enUS() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + resolver.setDefaultLocale(Locale.forLanguageTag("en")); + resolver.initCache(); + String message = resolver.getMessage( + "hello_language", + null, + Locale.forLanguageTag("en-US") + ); + assertEquals("Hello EN_US (messages)", message); + } + + @Test + void test_withRegion_fallback() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + resolver.setDefaultLocale(Locale.forLanguageTag("en")); + resolver.initCache(); + String message = resolver.getMessage( + "hello_language", + null, + Locale.forLanguageTag("en-GB") + ); + + assertEquals("Hello EN (messages)", message); + } + + @Test + void test_setSefaultDomain() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + resolver.setDefaultLocale(Locale.forLanguageTag("en")); + resolver.setDefaultDomain("otherdomain"); + resolver.initCache(); + String message = resolver.getMessage( + "hello_language", + null, + Locale.forLanguageTag("en-US") + ); + assertEquals("Other Hello EN (otherdomain)", message); + } + + @Test + void test_setBasenamePattern() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamesPattern(List.of("translations/*")); + resolver.setDefaultLocale(Locale.forLanguageTag("en")); + resolver.setDefaultDomain("otherdomain"); + resolver.initCache(); + String message = resolver.getMessage( + "hello_language", + null, + Locale.forLanguageTag("en-US") + ); + assertEquals("Other Hello EN (otherdomain)", message); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverFallbackTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverFallbackTest.java new file mode 100644 index 0000000..3dcf75f --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverFallbackTest.java @@ -0,0 +1,42 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class XliffMatchingResourcePatternResolverFallbackTest { + + @Test + void test_fallbackDefaultLanguage() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + resolver.setDefaultLocale(Locale.forLanguageTag("en")); + resolver.initCache(); + String message = resolver.getMessage( + "hello_language", + null, + Locale.forLanguageTag("jp") + ); + assertEquals("Hello EN (messages)", message); + } + + @Test + void test_getMessage_withDefaultMessage_messageNotExists_defaultMessageWithArgs() { + var resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + resolver.setBasenamePattern("translations/*"); + resolver.setDefaultLocale(Locale.forLanguageTag("en")); + resolver.initCache(); + + Object[] args = {"Road Runner", "Wile E. Coyote"}; + String message = resolver.getMessage( + "not_exists", + args, + "{0} and {1} as default", + Locale.forLanguageTag("jp") + ); + + assertEquals("Road Runner and Wile E. Coyote as default", message); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverInitCacheTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverInitCacheTest.java new file mode 100644 index 0000000..ef41c57 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverInitCacheTest.java @@ -0,0 +1,16 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import org.junit.jupiter.api.BeforeEach; + +import java.util.Locale; + +class XliffMatchingResourcePatternResolverInitCacheTest extends XliffMatchingResourcePatternResolverAbstract { + + @BeforeEach + void beforeEach() { + this.resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + this.resolver.setBasenamePattern("translations/*"); + this.resolver.setDefaultLocale(Locale.forLanguageTag("en")); + this.resolver.initCache(); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverTest.java new file mode 100644 index 0000000..55674f8 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/XliffMatchingResourcePatternResolverTest.java @@ -0,0 +1,15 @@ +package io.github.alaugks.spring.messagesource.xliff; + +import org.junit.jupiter.api.BeforeEach; + +import java.util.Locale; + +class XliffMatchingResourcePatternResolverTest extends XliffMatchingResourcePatternResolverAbstract { + + @BeforeEach + void beforeEach() { + this.resolver = new XliffTranslationMessageSource(TestUtilities.getMockedCacheManager()); + this.resolver.setBasenamePattern("translations/*"); + this.resolver.setDefaultLocale(Locale.forLanguageTag("en")); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogCacheTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogCacheTest.java new file mode 100644 index 0000000..ffc6af4 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogCacheTest.java @@ -0,0 +1,101 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import io.github.alaugks.spring.messagesource.xliff.TestUtilities; +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceCacheNotExistsException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cache.CacheManager; + +import java.util.HashMap; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class CatalogCacheTest { + private CatalogCache catalogCache; + private Locale locale; + + @BeforeEach + void beforeEach() { + this.catalogCache = new CatalogCache(TestUtilities.getMockedCacheManager()); + this.locale = Locale.forLanguageTag("en"); + } + + @Test + void test_initCache() { + Catalog catalog = new Catalog(); + catalog.put(this.locale, "domain", "code", "targetValue"); + this.catalogCache.initCache(catalog); + assertTrue(catalog.has(locale, "domain.code")); + } + + @Test + void test_getAll() { + assertInstanceOf(HashMap.class, this.catalogCache.getAll()); + } + + @Test + void test_put_get_withDomain() { + this.catalogCache.put(this.locale, "domain", "code", "targetValue"); + assertEquals("targetValue", this.catalogCache.get(locale, "domain.code")); + } + + @Test + void test_put_has_withDomain() { + Locale locale = Locale.forLanguageTag("en"); + this.catalogCache.put(this.locale, "domain", "code", "targetValue"); + assertTrue(this.catalogCache.has(locale, "domain.code")); + assertFalse(this.catalogCache.has(locale, "domain.bar")); + } + + @Test + void test_put_get() { + Locale locale = Locale.forLanguageTag("en"); + this.catalogCache.put(this.locale, "code", "targetValue"); + assertEquals("targetValue", this.catalogCache.get(locale, "code")); + } + + @Test + void test_put_has() { + Locale locale = Locale.forLanguageTag("en"); + this.catalogCache.put(this.locale, "code", "targetValue"); + assertTrue(this.catalogCache.has(locale, "code")); + assertFalse(this.catalogCache.has(locale, "bar")); + } + + @Test + void test_get_onNull() { + this.catalogCache.put(this.locale, "domain", "code", "targetValue"); + assertNull(this.catalogCache.get(locale, "domain.foo")); + } + + @Test + void test_exception_cacheNameNotExists() { + CacheManager cacheManager = TestUtilities.getMockedCacheManager("CACHE_NAME_NOT_EXISTS"); + + XliffMessageSourceCacheNotExistsException exception = assertThrows( + XliffMessageSourceCacheNotExistsException.class, () -> { + new CatalogCache(cacheManager); + } + ); + assertEquals( + "Cache with name [messagesource.xliff.catalog.CACHE] not available.", + exception.getMessage() + ); + } + + @Test + void test_exception() { + XliffMessageSourceCacheNotExistsException exception = assertThrows( + XliffMessageSourceCacheNotExistsException.class, () -> { + new CatalogCache(null); + } + ); + assertEquals("org.springframework.cache.CacheManager not available.", exception.getMessage()); + } + + @Test + void test_Constants() { + assertEquals("messagesource.xliff.catalog.CACHE", CatalogCache.CACHE_NAME); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogTest.java new file mode 100644 index 0000000..56e593c --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogTest.java @@ -0,0 +1,80 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class CatalogTest { + + static Catalog catalog = new Catalog(); + + @BeforeEach + void BeforeEach() { + // Domain foo + catalog.put(Locale.forLanguageTag("en"), "foo", "key_1", "value_en_1"); + catalog.put(Locale.forLanguageTag("en"), "foo", "key_2", "value_en_2"); + catalog.put(Locale.forLanguageTag("en"), "foo", "key_1", "value_en_3"); // Check overwrite + // Domain bar + catalog.put(Locale.forLanguageTag("en"), "bar", "key_1", "value_en_1"); + catalog.put(Locale.forLanguageTag("en"), "bar", "key_2", "value_en_2"); + catalog.put(Locale.forLanguageTag("en"), "bar", "key_1", "value_en_3"); // Check overwrite + // Domain foo + catalog.put(Locale.forLanguageTag("en-US"), "foo", "key_1", "value_en_us_1"); + catalog.put(Locale.forLanguageTag("en_US"), "foo", "key_2", "value_en_us_2"); + } + + @Test + void test_en() { + // Domain foo + Locale locale = Locale.forLanguageTag("en"); + assertEquals("value_en_1", catalog.get(locale, "foo.key_1")); + // Domain bar + assertEquals("value_en_1", catalog.get(locale, "bar.key_1")); + // Domain foo + assertEquals("value_en_2", catalog.get(locale, "foo.key_2")); + // Domain bar + assertEquals("value_en_2", catalog.get(locale, "bar.key_2")); + + // Domain bar + assertNull(catalog.get(locale, "bar.key_3")); + // Domain foo + assertNull(catalog.get(locale, "foo.key_3")); + } + + @Test + void test_localeExists() { + assertTrue(catalog.localeExists(Locale.forLanguageTag("en"))); + assertTrue(catalog.localeExists(Locale.forLanguageTag("en-US"))); + assertFalse(catalog.localeExists(Locale.forLanguageTag("en_US"))); + assertFalse(catalog.localeExists(Locale.forLanguageTag("jp"))); + } + + @Test + void test_enUk_withDash() { + Locale locale = Locale.forLanguageTag("en-US"); + // Domain foo + assertEquals("value_en_us_1", catalog.get(locale, "foo.key_1")); + } + + @Test + void test_enUk_withUnderscore() { + Locale locale = Locale.forLanguageTag("en_US"); + // Domain foo + assertNull(catalog.get(locale, "foo.key_2")); + } + + @Test + void test_hasTranslation_true() { + Locale locale = Locale.forLanguageTag("en"); + assertTrue(catalog.has(locale, "foo.key_1")); + } + + @Test + void test_hasTranslation_false() { + Locale locale = Locale.forLanguageTag("en"); + assertFalse(catalog.has(locale, "key_3")); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogUtilitiesTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogUtilitiesTest.java new file mode 100644 index 0000000..d655067 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogUtilitiesTest.java @@ -0,0 +1,64 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CatalogUtilitiesTest { + + @Test + void test_contactCode() { + assertEquals("domain.code", CatalogUtilities.contactCode("domain", "code")); + } + + @Test + void test_normalizeLocale() { + assertEquals("en", CatalogUtilities.localeToKey(Locale.forLanguageTag("en"))); + assertEquals("en", CatalogUtilities.localeToKey(Locale.forLanguageTag("EN"))); + assertEquals("en-gb", CatalogUtilities.localeToKey(Locale.forLanguageTag("en-gb"))); + assertEquals("en-gb", CatalogUtilities.localeToKey(Locale.forLanguageTag("en-GB"))); + assertEquals("en-gb", CatalogUtilities.localeToKey(Locale.forLanguageTag("EN-GB"))); + + Locale localeVariant1 = new Locale.Builder().setLanguage("de").setRegion("DE").setVariant("Cologne").build(); + assertEquals("de-de", CatalogUtilities.localeToKey(localeVariant1)); + + Locale localeVariant2 = Locale.forLanguageTag("de-Germany"); + assertEquals("de", CatalogUtilities.localeToKey(localeVariant2)); + + Locale localeVariant3 = Locale.forLanguageTag("hy-Latn-IT-arevela"); + assertEquals("hy-it", CatalogUtilities.localeToKey(localeVariant3)); + } + + @Test + void test_buildLocale_Locale() { + assertEquals("en", CatalogUtilities.buildLocale(Locale.forLanguageTag("en")).toString()); + assertEquals("en", CatalogUtilities.buildLocale(Locale.forLanguageTag("EN")).toString()); + assertEquals("en_GB", CatalogUtilities.buildLocale(Locale.forLanguageTag("en-gb")).toString()); + assertEquals("en_GB", CatalogUtilities.buildLocale(Locale.forLanguageTag("en-GB")).toString()); + assertEquals("en_GB", CatalogUtilities.buildLocale(Locale.forLanguageTag("EN-GB")).toString()); + + Locale localeVariant1 = new Locale.Builder().setLanguage("de").setRegion("DE").setVariant("Cologne").build(); + assertEquals("de_DE", CatalogUtilities.buildLocale(localeVariant1).toString()); + + Locale localeVariant2 = Locale.forLanguageTag("de-Germany"); + assertEquals("de", CatalogUtilities.buildLocale(localeVariant2).toString()); + + Locale localeVariant3 = Locale.forLanguageTag("hy-Latn-IT-arevela"); + assertEquals("hy_IT", CatalogUtilities.buildLocale(localeVariant3).toString()); + + Locale localeVariant4 = Locale.forLanguageTag("sr-Cyrl"); + assertEquals("sr", CatalogUtilities.buildLocale(localeVariant4).toString()); + } + + @Test + void test_buildLocale_languageRegion() { + assertEquals("en", CatalogUtilities.buildLocale("en", null).toString()); + assertEquals("", CatalogUtilities.buildLocale(null, "GB").toString()); + assertEquals("en", CatalogUtilities.buildLocale(Locale.forLanguageTag("EN")).toString()); + assertEquals("en_GB", CatalogUtilities.buildLocale("en", "gb").toString()); + assertEquals("en_GB", CatalogUtilities.buildLocale("en", "GB").toString()); + assertEquals("en_GB", CatalogUtilities.buildLocale("EN", "GB").toString()); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogWrapperTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogWrapperTest.java new file mode 100644 index 0000000..c331288 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/CatalogWrapperTest.java @@ -0,0 +1,43 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog; + +import io.github.alaugks.spring.messagesource.xliff.TestUtilities; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CatalogWrapperTest { + + private Locale locale; + private CatalogWrapper catalogWrapper; + + @BeforeEach + void beforeEach() { + this.locale = Locale.forLanguageTag("en"); + this.catalogWrapper = TestUtilities.getCacheWrapperWithCachedTestCatalog(); + } + + @Test + void test_get() { + assertEquals("Hello EN (messages)", this.catalogWrapper.get(locale, "hello_language").toString()); + // again + assertEquals("Hello EN (messages)", this.catalogWrapper.get(locale, "messages.hello_language").toString()); + // again + assertEquals("Hello EN (messages)", this.catalogWrapper.get(locale, "hello_language").toString()); + // again + assertEquals("Hello EN (messages)", this.catalogWrapper.get(locale, "messages.hello_language").toString()); + } + + @Test + void test_put() { + this.catalogWrapper.setDefaultDomain("foo"); + + this.catalogWrapper.put(locale, "foo", "code", "foo_value"); + assertEquals("foo_value", this.catalogWrapper.get(locale, "code").toString()); + + this.catalogWrapper.put(locale, "bar", "code", "bar_value"); + assertEquals("bar_value", this.catalogWrapper.get(locale, "bar.code").toString()); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff12Test.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff12Test.java new file mode 100644 index 0000000..f347a5d --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff12Test.java @@ -0,0 +1,39 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.TestUtilities; +import io.github.alaugks.spring.messagesource.xliff.catalog.Catalog; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class Xliff12Test { + + private Document document; + + @BeforeEach + void beforeEach() throws ParserConfigurationException, IOException, SAXException { + String filePath = "fixtures/xliff12.xliff"; + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filePath); + this.document = TestUtilities.getDocument(inputStream); + } + + @Test + void test_readXliffFile() { + Xliff12 version = new Xliff12(); + CatalogInterface catalog = new Catalog(); + Locale locale = Locale.forLanguageTag("en"); + version.read(catalog, document, "domain", locale); + + assertEquals("Hallo, Welt!", catalog.get(locale, "domain.code-1")); + assertEquals("Dies ist ein weiterer Satz.", catalog.get(locale, "domain.code-2")); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff20Test.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff20Test.java new file mode 100644 index 0000000..29eed3d --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff20Test.java @@ -0,0 +1,39 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.TestUtilities; +import io.github.alaugks.spring.messagesource.xliff.catalog.Catalog; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class Xliff20Test { + + private Document document; + + @BeforeEach + void beforeEach() throws ParserConfigurationException, IOException, SAXException { + String filePath = "fixtures/xliff20.xliff"; + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filePath); + this.document = TestUtilities.getDocument(inputStream); + } + + @Test + void test_readXliffFile() { + Xliff20 version = new Xliff20(); + CatalogInterface catalog = new Catalog(); + Locale locale = Locale.forLanguageTag("en"); + version.read(catalog, document, "domain", locale); + + assertEquals("Hallo, Welt!", catalog.get(locale, "domain.code-1")); + assertEquals("Dies ist ein weiterer Satz.", catalog.get(locale, "domain.code-2")); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff21Test.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff21Test.java new file mode 100644 index 0000000..6d7083a --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/Xliff21Test.java @@ -0,0 +1,39 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.TestUtilities; +import io.github.alaugks.spring.messagesource.xliff.catalog.Catalog; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class Xliff21Test { + + private Document document; + + @BeforeEach + void beforeEach() throws ParserConfigurationException, IOException, SAXException { + String filePath = "fixtures/xliff21.xliff"; + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filePath); + this.document = TestUtilities.getDocument(inputStream); + } + + @Test + void test_readXliffFile() { + Xliff21 version = new Xliff21(); + CatalogInterface catalog = new Catalog(); + Locale locale = Locale.forLanguageTag("en"); + version.read(catalog, document, "domain", locale); + + assertEquals("Hallo, Welt!", catalog.get(locale, "domain.code-1")); + //assertEquals("Dies ist ein weiterer Satz.", catalog.get(locale, "code-2")); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffCatalogBuilderTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffCatalogBuilderTest.java new file mode 100644 index 0000000..31644a4 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffCatalogBuilderTest.java @@ -0,0 +1,61 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import io.github.alaugks.spring.messagesource.xliff.catalog.Catalog; +import io.github.alaugks.spring.messagesource.xliff.catalog.CatalogInterface; +import io.github.alaugks.spring.messagesource.xliff.exception.XliffMessageSourceVersionSupportException; +import io.github.alaugks.spring.messagesource.xliff.ressources.ResourcesLoader; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class XliffCatalogBuilderTest { + + @Test + void test_createCatalog() { + ResourcesLoader resourcesLoader = new ResourcesLoader(); + resourcesLoader.setBasenamePattern("translations/*"); + resourcesLoader.setDefaultLocale(Locale.forLanguageTag("en")); + XliffCatalogBuilder xliffCatalogBuilder = new XliffCatalogBuilder(); + CatalogInterface catalog = xliffCatalogBuilder.createCatalog(resourcesLoader, new Catalog()); + assertEquals("Hello EN (messages)", catalog.get(Locale.forLanguageTag("en"), "messages.hello_language")); + } + + @Test + void test_createCatalog_versionNotSupported() { + Catalog catalog = new Catalog(); + ResourcesLoader resourcesLoader = new ResourcesLoader(); + resourcesLoader.setBasenamePattern("fixtures/*"); + resourcesLoader.setDefaultLocale(Locale.forLanguageTag("en-GB")); + XliffCatalogBuilder xliffCatalogBuilder = new XliffCatalogBuilder(); + + XliffMessageSourceVersionSupportException exception = assertThrows( + XliffMessageSourceVersionSupportException.class, () -> { + xliffCatalogBuilder.createCatalog(resourcesLoader, catalog); + } + ); + assertEquals("XLIFF version \"1.0\" not supported.", exception.getMessage()); + } + + @Test + @Disabled("Todo: Handling [Fatal Error] :9:7: The element type \"body\" must be terminated by the matching end-tag \"\".") + void test_createCatalog_parseError() { + // ErrorHandler errorHandler = new SimpleSaxErrorHandler(new NoOpLog()); + var catalog = new Catalog(); + ResourcesLoader resourcesLoader = new ResourcesLoader(); + resourcesLoader.setBasenamePattern("translations_broken/*"); + resourcesLoader.setDefaultLocale(Locale.forLanguageTag("en")); + XliffCatalogBuilder xliffCatalogBuilder = new XliffCatalogBuilder(); + + Throwable exception = assertThrows( + Throwable.class, () -> { + xliffCatalogBuilder.createCatalog(resourcesLoader, catalog); + } + ); + //assertTrue(exception.getMessage().indexOf("body") > 0); + //assertEquals("The element type \"body\" must be terminated by the matching end-tag \"\".", exception.getMessage()); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffParserUtilityTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffParserUtilityTest.java new file mode 100644 index 0000000..92d8cfb --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffParserUtilityTest.java @@ -0,0 +1,60 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class XliffParserUtilityTest { + @Test + void test_createCode() { + assertEquals("my-resname", XliffParserUtility.getCode(null, "my-resname", "my-target-value")); + assertEquals("my-resname", XliffParserUtility.getCode("my-id", "my-resname", "my-target-value")); + assertEquals("my-id", XliffParserUtility.getCode("my-id", null, "my-target-value")); + } + + @Test + void test_getElementValue_getCharacterDataFromElement_TextNode() { + String value; + value = XliffParserUtility.getElementValue(getRootElement(), "element"); + assertEquals("value", value); + value = XliffParserUtility.getElementValue(getRootElement(), "element-newline"); + assertEquals("value", value); + value = XliffParserUtility.getElementValue(getRootElement(), "element-with-cdata"); + assertEquals("value", value); + value = XliffParserUtility.getElementValue(getRootElement(), "element-with-cdata-newline"); + assertEquals("value", value); + } + + @Test + void test_getElementValue_getCharacterDataFromElement_Node() { + String value = XliffParserUtility.getElementValue(getRootElement(), "dummy"); + assertNull(value); + } + + private static Element getRootElement() { + try { + InputStream fileStream = XliffParserUtilityTest + .class + .getClassLoader() + .getResourceAsStream("fixtures/xliff-parser-utility-test.xliff"); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(fileStream); + return document.getDocumentElement(); + } catch (ParserConfigurationException | IOException | SAXException e) { + return null; + } + } + +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffReaderTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffReaderTest.java new file mode 100644 index 0000000..855e0b7 --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/catalog/xliff/XliffReaderTest.java @@ -0,0 +1,29 @@ +package io.github.alaugks.spring.messagesource.xliff.catalog.xliff; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +class XliffReaderTest { + + private XliffReader reader; + + @BeforeEach + void beforeEach() { + this.reader = new XliffReader(); + } + + @Test + void test_supportedVersions() { + assertInstanceOf(Xliff12.class, this.reader.getReader("1.2")); + assertInstanceOf(Xliff20.class, this.reader.getReader("2.0")); + assertInstanceOf(Xliff21.class, this.reader.getReader("2.1")); + } + + @Test + void test_versionNotSupported() { + assertNull(this.reader.getReader("1.0")); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesFileNameParserTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesFileNameParserTest.java new file mode 100644 index 0000000..b15743c --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesFileNameParserTest.java @@ -0,0 +1,60 @@ +package io.github.alaugks.spring.messagesource.xliff.ressources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ResourcesFileNameParserTest { + + @Test + void test_domain_withoutLocale() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message").parse(); + assertEquals("message", filename.getDomain()); + assertNull(filename.getLanguage()); + assertNull(filename.getRegion()); + } + + @Test + void test_domain_en() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message_en").parse(); + assertEquals("message", filename.getDomain()); + assertEquals("en", filename.getLanguage()); + assertNull(filename.getRegion()); + } + + @Test + void test_domain_en_withDash() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message-en").parse(); + assertEquals("message", filename.getDomain()); + assertEquals("en", filename.getLanguage()); + assertNull(filename.getRegion()); + } + + @Test + void test_domain_enGB() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message_en_GB").parse(); + assertEquals("message", filename.getDomain()); + assertEquals("en", filename.getLanguage()); + assertEquals("GB", filename.getRegion()); + } + + @Test + void test_domain_enGB_withDash() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message-en-GB").parse(); + assertEquals("message", filename.getDomain()); + assertEquals("en", filename.getLanguage()); + assertEquals("GB", filename.getRegion()); + } + + @Test + void test_hasNoLocale() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message").parse(); + assertFalse(filename.hasLocale()); + } + + @Test + void test_hasLocale() { + ResourcesFileNameParser.Dto filename = new ResourcesFileNameParser("message_de").parse(); + assertTrue(filename.hasLocale()); + } +} diff --git a/src/test/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoaderTest.java b/src/test/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoaderTest.java new file mode 100644 index 0000000..8f4e10b --- /dev/null +++ b/src/test/java/io/github/alaugks/spring/messagesource/xliff/ressources/ResourcesLoaderTest.java @@ -0,0 +1,73 @@ +package io.github.alaugks.spring.messagesource.xliff.ressources; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +class ResourcesLoaderTest { + + ArrayList resources; + private ResourcesLoader resourcesLoader; + + @BeforeEach + void beforeEach() throws IOException { + this.resourcesLoader = new ResourcesLoader(); + this.resourcesLoader.setDefaultLocale(Locale.forLanguageTag("en")); + + } + + @Test + void test_setBasenamePattern() throws IOException { + this.resourcesLoader.setBasenamePattern( + "translations/*" + ); + assertEquals(5, this.resourcesLoader.getResourcesInputStream().size()); + } + + @Test + void test_setBasenamePattern_domainMessages() throws IOException { + this.resourcesLoader.setBasenamePattern( + "translations/messages*" + ); + assertEquals(3, this.resourcesLoader.getResourcesInputStream().size()); + } + + + @Test + void test_setBasenamePattern_languageDe() throws IOException { + this.resourcesLoader.setBasenamePattern( + "translations/*_de*" + ); + assertEquals(2, this.resourcesLoader.getResourcesInputStream().size()); + } + + @Test + void test_setBasenamesPattern() throws IOException { + this.resourcesLoader.setBasenamesPattern( + List.of( + "translations_en/*", + "translations_de/*" + ) + ); + assertEquals(4, this.resourcesLoader.getResourcesInputStream().size()); + } + + @Test + void test_Dto() throws IOException { + this.resourcesLoader.setBasenamePattern( + "translations_en_US/*" + ); + ResourcesLoader.Dto dto = this.resourcesLoader.getResourcesInputStream().get(0); + assertEquals("messages", dto.getDomain()); + assertEquals("en_US", dto.getLocale().toString()); + assertInstanceOf(InputStream.class, dto.getInputStream()); + } +} diff --git a/src/test/resources/fixtures/xliff-parser-utility-test.xliff b/src/test/resources/fixtures/xliff-parser-utility-test.xliff new file mode 100644 index 0000000..9a6a827 --- /dev/null +++ b/src/test/resources/fixtures/xliff-parser-utility-test.xliff @@ -0,0 +1,14 @@ + + + value + + value + + + + + + + diff --git a/src/test/resources/fixtures/xliff10.xliff b/src/test/resources/fixtures/xliff10.xliff new file mode 100644 index 0000000..1f28738 --- /dev/null +++ b/src/test/resources/fixtures/xliff10.xliff @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/test/resources/fixtures/xliff12.xliff b/src/test/resources/fixtures/xliff12.xliff new file mode 100644 index 0000000..17cae9b --- /dev/null +++ b/src/test/resources/fixtures/xliff12.xliff @@ -0,0 +1,21 @@ + + + + + + Hello, world! + Hallo, Welt! + + + This is another sentence. + Dies ist ein weiterer Satz. + + + + diff --git a/src/test/resources/fixtures/xliff20.xliff b/src/test/resources/fixtures/xliff20.xliff new file mode 100644 index 0000000..8d7bf74 --- /dev/null +++ b/src/test/resources/fixtures/xliff20.xliff @@ -0,0 +1,18 @@ + + + + + + Hello, world! + Hallo, Welt! + + + This is another sentence. + Dies ist ein weiterer Satz. + + + + diff --git a/src/test/resources/fixtures/xliff21.xliff b/src/test/resources/fixtures/xliff21.xliff new file mode 100644 index 0000000..4338b4a --- /dev/null +++ b/src/test/resources/fixtures/xliff21.xliff @@ -0,0 +1,14 @@ + + + + + + Hello, world! + Hallo, Welt! + + + + diff --git a/src/test/resources/translations/.xliff b/src/test/resources/translations/.xliff new file mode 100644 index 0000000..47a012b --- /dev/null +++ b/src/test/resources/translations/.xliff @@ -0,0 +1,29 @@ + + + + + + + Hello EN (messages) + + + Hello EN (messages) + + + + + {0} and {1} + + + {0} and {1} + + + + + diff --git a/src/test/resources/translations/messages.properties b/src/test/resources/translations/messages.properties new file mode 100644 index 0000000..ee052e5 --- /dev/null +++ b/src/test/resources/translations/messages.properties @@ -0,0 +1 @@ +hello_language=This is only for a jUnit-Test diff --git a/src/test/resources/translations/messages.xliff b/src/test/resources/translations/messages.xliff new file mode 100644 index 0000000..4958922 --- /dev/null +++ b/src/test/resources/translations/messages.xliff @@ -0,0 +1,27 @@ + + + + + + + Hello EN (messages) + + + Hello EN (messages) + + + + + {0} and {1} + + + {0} and {1} + + + + + diff --git a/src/test/resources/translations/messages_de.xliff b/src/test/resources/translations/messages_de.xliff new file mode 100644 index 0000000..879ec84 --- /dev/null +++ b/src/test/resources/translations/messages_de.xliff @@ -0,0 +1,28 @@ + + + + + + + + Hello EN (messages) + + + Hallo DE (messages) + + + + + {0} and {1} + + + {0} und {1} + + + + + diff --git a/src/test/resources/translations/messages_en_US.xliff b/src/test/resources/translations/messages_en_US.xliff new file mode 100644 index 0000000..40a2adc --- /dev/null +++ b/src/test/resources/translations/messages_en_US.xliff @@ -0,0 +1,27 @@ + + + + + + + Hello EN (messages) + + + Hello EN_US (messages) + + + + + {0} and {1} + + + {0} and {1} + + + + + diff --git a/src/test/resources/translations/otherdomain.xlf b/src/test/resources/translations/otherdomain.xlf new file mode 100644 index 0000000..ee6164e --- /dev/null +++ b/src/test/resources/translations/otherdomain.xlf @@ -0,0 +1,19 @@ + + + + + + + Hello EN (otherdomain) + + + Other Hello EN (otherdomain) + + + + + diff --git a/src/test/resources/translations/otherdomain_de.xlf b/src/test/resources/translations/otherdomain_de.xlf new file mode 100644 index 0000000..5de1412 --- /dev/null +++ b/src/test/resources/translations/otherdomain_de.xlf @@ -0,0 +1,20 @@ + + + + + + + + Hello EN (otherdomain) + + + Hallo DE (otherdomain) + + + + + diff --git a/src/test/resources/translations_broken/messages_en_US.xliff b/src/test/resources/translations_broken/messages_en_US.xliff new file mode 100644 index 0000000..ffd6c2e --- /dev/null +++ b/src/test/resources/translations_broken/messages_en_US.xliff @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/test/resources/translations_de/messages_de.xliff b/src/test/resources/translations_de/messages_de.xliff new file mode 100644 index 0000000..f5786d4 --- /dev/null +++ b/src/test/resources/translations_de/messages_de.xliff @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/test/resources/translations_de/otherdomain_de.xlf b/src/test/resources/translations_de/otherdomain_de.xlf new file mode 100644 index 0000000..f5786d4 --- /dev/null +++ b/src/test/resources/translations_de/otherdomain_de.xlf @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/test/resources/translations_en/messages.xliff b/src/test/resources/translations_en/messages.xliff new file mode 100644 index 0000000..3aa0b87 --- /dev/null +++ b/src/test/resources/translations_en/messages.xliff @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/test/resources/translations_en/otherdomain.xlf b/src/test/resources/translations_en/otherdomain.xlf new file mode 100644 index 0000000..3aa0b87 --- /dev/null +++ b/src/test/resources/translations_en/otherdomain.xlf @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/test/resources/translations_en_US/messages_en_US.xliff b/src/test/resources/translations_en_US/messages_en_US.xliff new file mode 100644 index 0000000..3aa0b87 --- /dev/null +++ b/src/test/resources/translations_en_US/messages_en_US.xliff @@ -0,0 +1,11 @@ + + + + + + +