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
- Version
- Dependency
- MessageSource Configuration
- CacheManager Configuration
- Cache warming with an ApplicationRunner (recommended)
- XLIFF Translation Files
- Using the MessageSource
- Full Example
- Support
- More Information
Version | Description |
---|---|
1.2.1 | Release notes |
1.2.0 | Release notes |
1.1.2 | Release notes |
1.1.1 | Release notes |
1.1.0 | Release notes |
1.0.0 | First public version |
Version | Description |
---|---|
2.0.0-SNAPSHOT | SNAPSHOT |
Maven
<dependency>
<groupId>io.github.alaugks</groupId>
<artifactId>spring-messagesource-xliff</artifactId>
<version>1.2.1</version>
</dependency>
Gradle
implementation group: 'io.github.alaugks', name: 'spring-messagesource-xliff', version: '1.2.1'
The class XliffTranslationMessageSource implements the MessageSource interface. An instance of the CacheManager is required for caching the translations.
setBasenamePattern(String basename)
or setBasenamesPattern(Iterable<String> basenames)
(required)
- Defines the pattern used to select the XLIFF files.
- The package uses the PathMatchingResourcePatternResolver to select the XLIFF files. So you can use the supported patterns.
- Files with the extension
xliff
andxlf
are filtered from the result list.
setDefaultLocale(Locale locale)
(required)
- Defines the default language.
setDefaultDomain(String defaultDomain)
- Defines the default domain. Default is
messages
. For more information, see XlIFF Translations Files.
Please note the CacheManager Configuration.
import de.alaugks.spring.XliffTranslationMessageSource;
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 {
public MessageSource messageSource(CacheManager cacheManager) {
XliffTranslationMessageSource messageSource = new XliffTranslationMessageSource(cacheManager);
messageSource.setDefaultLocale(Locale.forLanguageTag("en"));
messageSource.setBasenamePattern("translations/*");
return messageSource;
}
}
You may already have an existing CacheManager configuration. If not, the following minimum CacheManager configuration is required. All Supported Cache Providers can also be used. Here is an example using Caffeine.
The CacheName must be set with the constant CatalogCache.CACHE_NAME
. The specific cache identifier is stored in the constant.
ConcurrentMapCacheManager is the default cache in Spring Boot and Spring.
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() {
return new ConcurrentMapCacheManager(CatalogCache.CACHE_NAME);
}
}
In the following example, the cache of translations is warmed up after the application starts.
import io.github.alaugks.spring.messagesource.xliff.XliffMessageSourcePatternResolver;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
@Component
public class MessageSourceCacheWarmUp implements ApplicationRunner {
private final MessageSource messageSource;
public AppStartupRunner(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
public void run(ApplicationArguments args) {
if (this.messageSource instanceof XliffTranslationMessageSource) {
((XliffTranslationMessageSource) this.messageSource).initCache();
}
}
}
- 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
orxlf
. - In the XLIFF files, the
<target/>
is retrieved in a<trans-unit/>
(XLIFF 1.2) or<segment/>
(XLIFF 2.*).- XLIFF 1.2:
- If the attribute
resname
does not exist, the attributeid
is used to determine the identifier. - Documentation identifier: XLIFF 1.2
- If the attribute
- XLIFF 2.*:
- XLIFF 1.2:
- For performance reasons, there is no validation of XLIFF files with an XMLSchema. If there is any corrupt XML in an XLIFF file, the SAX parser will throw a [Fatal Error].
# Default language
<domain>.xlf
# Domain + Language
<domain>[-_]<language>.xlf
# Domain + Language + Region
<domain>[-_]<language>[-_]<region>.xlf
- Default domain is
messages
. - Default locale is
en
without region. - Translations are provided for the locale
de
(without region) anden-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
Mixing XLIFF versions is possible. Here is an example using XLIFF 1.2 and XLIFF 2.1.
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2"
xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en"
target-language="en">
<body>
<trans-unit id="headline">
<source>Headline</source>
<target>Headline</target>
</trans-unit>
<trans-unit id="postcode">
<source>Postcode</source>
<target>Postcode</target>
</trans-unit>
<trans-unit id="email-notice">
<source>Your email {0} has been registered.</source>
<target>Your email {0} has been registered.</target>
</trans-unit>
<trans-unit id="default-message">
<source>This is a default message.</source>
<target>This is a default message.</target>
</trans-unit>
</body>
</file>
</xliff>
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2"
xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en"
target-language="de">
<body>
<trans-unit id="headline">
<source>Headline</source>
<target>Überschrift</target>
</trans-unit>
<trans-unit id="postcode">
<source>Postcode</source>
<target>Postleitzahl</target>
</trans-unit>
<trans-unit id="email-notice">
<source>Your email {0} has been registered.</source>
<target>Ihre E-Mail {0} wurde registriert.</target>
</trans-unit>
<trans-unit id="default-message">
<source>This is a default message.</source>
<target>Das ist ein Standardtext.</target>
</trans-unit>
</body>
</file>
</xliff>
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2"
xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en"
target-language="en-US">
<body>
<trans-unit id="postcode">
<source>Postcode</source>
<target>Zip code</target>
</trans-unit>
</body>
</file>
</xliff>
<?xml version="1.0" encoding="UTF-8" ?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.1" version="2.1"
srcLang="en" trgLang="en">
<file id="payment">
<unit>
<segment id="headline">
<source>Payment</source>
<target>Payment</target>
</segment>
<segment id="expiry_date">
<source>Expiry date</source>
<target>Expiry date</target>
</segment>
</unit>
</file>
</xliff>
<?xml version="1.0" encoding="UTF-8" ?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.1" version="2.1"
srcLang="en" trgLang="de">
<file id="payment_de">
<unit>
<segment id="headline">
<source>Payment</source>
<target>Zahlung</target>
</segment>
<segment id="expiry_date">
<source>Expiry date</source>
<target>Ablaufdatum</target>
</segment>
</unit>
</file>
</xliff>
<?xml version="1.0" encoding="UTF-8" ?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.1" version="2.1"
srcLang="en" trgLang="en-US">
<file id="payment_en-US">
<unit>
<segment id="expiry_date">
<source>Expiry date</source>
<target>Expiration date</target>
</segment>
</unit>
</file>
</xliff>
id | en | en-US | de | jp*** |
---|---|---|---|---|
headline* messages.headline |
Headline | Headline** | Überschrift | Headline |
postcode* messages.postcode |
Postcode | Zip code | Postleitzahl | Postcode |
email-notice* messages.email-notice |
Your email {0} has been registered. | Your email {0} has been registered.** | Ihre E-Mail {0} wurde registriert. | Your email {0} has been registered. |
default-message* messages.default-message |
This is a default message. | This is a default message.** | Das ist ein Standardtext. | This is a default message. |
payment.headline | Payment | Payment** | Zahlung | Payment |
payment.expiry_date | Expiry date | Expiration date | Ablaufdatum | Expiry date |
*Default domain is
messages
.**Example of a fallback from Language_Region (
en-US
) to Language (en
). Theid
does not exist inen-US
, so it tries to select the translation with localeen
.***There is no translation for Japanese (
jp
). The default locale translations (en
) are selected.
With the implementation and use of the MessageSource interface, the translations are also available in Thymeleaf, as Service (Dependency Injection) and Custom Validation Messages. Also in packages and implementations that use the MessageSource.
With the configured MessageSource, the translations are available in Thymeleaf. See the example in the Full Example.
<!-- Default domain: messages -->
<!-- "Headline" -->
<h1 th:text="#{headline}"/>
<h1 th:text="#{messages.headline}"/>
<!-- "Postcode" -->
<label th:text="#{postcode}"/>
<label th:text="#{messages.postcode}"/>
<!-- "Your email john.doe@example.com has been registered." -->
<span th:text="#{email-notice('john.doe@example.com')}"/>
<span th:text="#{messages.email-notice('john.doe@example.com')}"/>
<!-- "This is a default message." -->
<span th:text="${#messages.msgOrNull('not-exists-id')} ?: #{default-message}"/>
<span th:text="${#messages.msgOrNull('not-exists-id')} ?: #{messages.default-message}"/>
<!-- Domain: payment -->
<!-- "Payment" -->
<h2 th:text="#{payment.headline}"/>
<!-- "Expiry date" -->
<strong th:text="#{payment.expiry_date}"/>
The MessageSource can be set via Autowire to access the translations. See the example in the Full Example.
import org.springframework.context.MessageSource;
private final MessageSource messageSource;
// Autowire MessageSource
public MyClass(MessageSource messageSource) {
this.messageSource = messageSource;
}
// Default domain: messages
// "Headline"
this.messageSource.getMessage("headline", null, locale);
this.messageSource.getMessage("messages.headline", null, locale);
// "Postcode"
this.messageSource.getMessage("postcode", null, locale);
this.messageSource.getMessage("messages.postcode", null, locale);
// "Your email john.doe@example.com has been registered."
Object[] args = {"john.doe@example.com"};
this.messageSource.getMessage("email-notice", args, locale);
this.messageSource.getMessage("messages.email-notice", args, locale);
// "This is a default message."
//String defaultMessage = this.messageSource.getMessage("default-message", null, locale);
String defaultMessage = this.messageSource.getMessage("messages.default-message", null, locale);
this.messageSource.getMessage("not-exists-id", null, defaultMessage, locale);
// Domain: payment
// "Payment"
this.messageSource.getMessage("payment.headline", null, locale);
// "Expiry date"
this.messageSource.getMessage("payment.expiry-date", null, locale);
The article Custom Validation MessageSource in Spring Boot describes how to use custom validation messages.
A Full Example using Spring Boot, mixing XLIFF 1.2 and XLIFF 2.1 translation files:
Repository: https://github.com/alaugks/spring-messagesource-xliff-example
Website: https://spring-boot-xliff-example.alaugks.dev
If you have questions, comments or feature requests please use the Discussions section.