This package provides a MessageSource interface for translations stored in XLIFF files. It supports XLIFF versions 1.2, 2.0, 2.1 and 2.2, including the PGS Module.
- 🚨 Migration: 3.x → 4.0 (Domain Removed)
- How it works
- Dependency
- MessageSource Configuration
- Example
- XLIFF Files
- Full Example
- Related MessageSources and Examples
- License
The XLIFF files are read once at startup and held in memory. Message resolution follows Spring's ResourceBundleMessageSource.
- Loading:
build()reads all matching XLIFF files and turns every unit into alocale/code/valueentry, grouped per locale. If a code occurs more than once for the same locale, the first one wins. - Locale fallback: For each requested locale, a
java.util.ResourceBundleis created on top of these entries. The standardResourceBundlelookup applies:en-US→en→ default locale.
Like Spring's ResourceBundleMessageSource, a null locale falls back to the default locale and a MessageSourceResolvable tries its codes in order. If nothing is found, getMessage() throws a NoSuchMessageException unless a default message is given.
<dependency>
<groupId>io.github.alaugks</groupId>
<artifactId>spring-messagesource-xliff</artifactId>
<version>4.2.0</version>
</dependency>implementation group: 'io.github.alaugks', name: 'spring-messagesource-xliff', version: '4.2.0'
| Method | Default | Description |
|---|---|---|
builder(Locale defaultLocale, String locationPattern) |
— | Entry point.defaultLocale is the locale to fall back to when a translation is missing.locationPattern selects the XLIFF files via Spring's PathMatchingResourcePatternResolver, so all its patterns work. |
builder(Locale defaultLocale, List<String> locationPatterns) |
— | Entry point (alternative). Same as above, with a list of patterns. |
fileExtensions(List<String> fileExtensions) |
List.of("xlf", "xliff") |
File extensions recognised as XLIFF files. Only matching files are kept. An empty list matches no file. |
enableSchemaValidation() |
disabled | Validates each file against its OASIS XSD before reading and rejects non-conforming files. Strict schemas also reject otherwise-readable files, e.g. XLIFF 1.2 <trans-unit/> without the required id.🚨 Recommended for development and testing, see the example configuration. |
strictMode(StrictMode... strictMode) |
none (lenient) | Enables checks that reject invalid values with an XliffMessageSourceRuntimeException instead of reading them best-effort. Available: StrictMode.TARGET_ORDER (non-numeric or non-continuous, not 1..n, target/@order, XLIFF 2.x). |
useXliffLanguageAttribute() |
disabled | Resolves the target locale from the language attribute of the XLIFF document (target-language for XLIFF 1.2, trgLang for XLIFF 2.x) instead of the filename suffix. See Determining the Target Locale. |
targetLocaleResolver(TargetLocaleResolverInterface targetLocaleResolver) |
— | Overrides how the target locale of a file is determined, in place of both the filename suffix and useXliffLanguageAttribute(). See Custom Target Locale Resolver. |
enableICU4j() |
disabled | Formats messages with ICU4J's com.ibm.icu.text.MessageFormat instead of java.text.MessageFormat. Adds named arguments and ICU plural/select/gender patterns. Required for the XLIFF 2.2 PGS module, see the note below. |
parentMessageSource(MessageSource parentMessageSource) |
— | Sets a parent MessageSource to delegate to when a code cannot be resolved from the XLIFF files.🚨 See Parent MessageSource. |
Important
The XLIFF 2.2 PGS module generates ICU patterns with named arguments (e.g. {count, plural, …}). These cannot be resolved by the default java.text.MessageFormat and fail at getMessage() time. When using the PGS module you must enable ICU4J via enableICU4j().
ICU4J is the com.ibm.icu:icu4j dependency, which is shipped transitively with this library. No extra dependency is required. Its com.ibm.icu.text.MessageFormat is a syntax superset of java.text.MessageFormat, so existing numeric-index patterns keep working.
Note that the two are not fully output-compatible: ICU4J uses Unicode CLDR locale data, so the formatted result for a given locale can differ from the JDK's, for example the decimal and grouping separators in numbers (. vs ,). Verify locale-sensitive output after enabling ICU4J.
- Default locale is
en. - The XLIFF files are stored in
src/main/resources/translations.
import io.github.alaugks.spring.messagesource.xliff.XliffResourceMessageSource;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Locale;
@Configuration
public class MessageSourceConfig {
@Bean
public MessageSource messageSource() {
return XliffResourceMessageSource
.builder(
Locale.forLanguageTag("en"),
"translations/*" // or List.of(...)
)
.build();
}
}- Default locale is
enwithout region. - Translations are provided for the locale
en,deanden-US.
[resources]
|-[translations]
|-messages.xliff // messages_en.xliff also works.
|-messages_de.xliff
|-messages_en-US.xliff
Tip
Trans-units can be organized across multiple XLIFF files however you like (e.g. by feature or module). This example keeps everything in one file per locale. The only requirement: resname / name must be unique across all files, since it is the key.
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2"
xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file original="messages"
datatype="plaintext"
source-language="en"
target-language="en">
<body>
<trans-unit id="1" resname="headline">
<source>Headline</source>
<target>Headline</target>
</trans-unit>
<trans-unit id="2" resname="postcode">
<source>Postcode</source>
<target>Postcode</target>
</trans-unit>
<trans-unit id="3" resname="payment.headline">
<source>Payment</source>
<target>Payment</target>
</trans-unit>
<trans-unit id="4" resname="payment.expiry_date">
<source>Expiry date</source>
<target>Expiry date</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 original="messages"
datatype="plaintext"
source-language="en"
target-language="de">
<body>
<trans-unit id="1" resname="headline">
<source>Headline</source>
<target>Überschrift</target>
</trans-unit>
<trans-unit id="2" resname="postcode">
<source>Postcode</source>
<target>Postleitzahl</target>
</trans-unit>
<trans-unit id="3" resname="payment.headline">
<source>Payment</source>
<target>Zahlung</target>
</trans-unit>
<trans-unit id="4" resname="payment.expiry_date">
<source>Expiry date</source>
<target>Ablaufdatum</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 original="messages"
datatype="plaintext"
source-language="en"
target-language="en-US">
<body>
<trans-unit id="2" resname="postcode">
<source>Postcode</source>
<target>Zip code</target>
</trans-unit>
<trans-unit id="4" resname="payment.expiry_date">
<source>Expiry date</source>
<target>Expiration date</target>
</trans-unit>
</body>
</file>
</xliff>The target locale is determined as described in Determining the Target Locale.
Resolving a value by code behaves like Spring's ResourceBundleMessageSource / ReloadableResourceBundleMessageSource.
| id (code) | en | en-US | de | jp** |
|---|---|---|---|---|
| headline | Headline | Headline* | Überschrift | Headline |
| postcode | Postcode | Zip code | Postleitzahl | Postcode |
| payment.headline | Payment | Payment* | Zahlung | Payment |
| payment.expiry_date | Expiry date | Expiration date | Ablaufdatum | Expiry date |
*Example of a fallback from Language_Region (
en-US) to Language (en). Theiddoes 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.
- Translations can be split across multiple files. The key is always taken from the unit itself (see Translation Key), the filename has no effect on it. Since the key is what's looked up, it must be unique across all files:
resname/name, oridif neither is set. - Files are selected by
locationPatternand by file extension:xlfandxliffby default, configurable withfileExtensions(). - Supported versions:
1.2,2.0,2.1and2.2. - Each file can optionally be validated against its OASIS XSD schema (1.2 →
xliff-core-1.2-transitional.xsd, 2.0/2.1 →xliff-core-2.0.xsd, 2.2 →xliff_core_2.2.xsdwith themetadata.xsdmodule). It is off by default and enabled withenableSchemaValidation(). The PGS module attributes are this library's extension and are not part of the OASIS core schema, so they are removed before validation. - 🚨 Valid XLIFF is a prerequisite. The library does not defend against invalid input. Such files may fail with an exception or produce undefined results. Enable the schema validation with
enableSchemaValidation()during development and in tests to catch invalid files early. See the example configuration. - SAX parser errors are handled by an ErrorHandler.
- Each unit yields a key (message code) and a value (the
<target/>text, or the<source/>text if the unit has no<target/>). The key is always the resource name (resname/name, withidas fallback), never the<source/>text. See Translation Key and Translation Value.
The key is the application-facing resource name. XLIFF separates the internal identifier (id) from the resource name (resname / name). The resource name is the key, with id as fallback when it is absent.
| Version | Element | 1. Key from | 2. Fallback | Not used as key |
|---|---|---|---|---|
| 1.2 | <trans-unit/> |
resname (optional, resource name) |
id (required, document identifier) |
— |
| 2.x | <unit/> |
name (optional, resource name) |
id (required, document identifier) |
segment/@id |
- XLIFF 1.2:
resname→id.resnameis the original resource name (e.g. a properties-file key) and is preferred;idis required and unique within the<file/>but is a tool-internal identifier, used as the key only whenresnameis absent. (Docs: General Identifiers) - XLIFF 2.x:
unit/@name→unit/@id, analogous to 1.2.segment/@idis never the key (optional, only unique within its<unit/>). (Docs: 2.0, 2.1, 2.2) - A unit is skipped when neither attribute is set (1.2: no
resname/id, 2.x: noname/id).
The value is the <target/> text and falls back to the <source/> text when no <target/> is present. It is the element's text content. Embedded markup (e.g. HTML as CDATA or escaped) is kept verbatim, XLIFF inline elements are reconstructed as plain text, and the value is trimmed unless xml:space="preserve" is set. See XLIFF Inline Elements and Markup and Whitespace (both apply to XLIFF 1.2 and 2.x).
Each <trans-unit/> has exactly one <source/> and one optional <target/>. The value is taken directly.
<trans-unit id="1" resname="greeting">
<source>Hello World</source>
<target>Hallo Welt</target>
</trans-unit>Result: greeting → Hallo Welt
A <unit/> holds one or more <segment/> elements. A single segment is the common case. Multiple segments are reassembled into one string. An <ignorable/> between them holds non-translatable content, typically whitespace.
The reassembly rules are:
- Each
<segment/>contributes its<target/>text, falling back to<source/>when no<target/>is present. - Each
<ignorable/>contributes its<source/>verbatim. - Parts are concatenated in document order.
<unit id="1" name="disclaimer">
<segment>
<source>All prices include VAT.</source>
<target>Alle Preise inkl. MwSt.</target>
</segment>
<ignorable>
<source> </source>
</ignorable>
<segment>
<source>Errors excepted.</source>
<target>Irrtümer vorbehalten.</target>
</segment>
</unit>Result: disclaimer → Alle Preise inkl. MwSt. Irrtümer vorbehalten.
The order attribute on <target/> defines how target segments are composed. Segments are sorted ascending by their order value. An <ignorable/> element always keeps its document position.
<unit id="1" name="example">
<segment>
<source>First</source>
<target order="2">Zweites</target>
</segment>
<ignorable>
<source> </source>
</ignorable>
<segment>
<source>Second</source>
<target order="1">Erstes</target>
</segment>
</unit>Result: example → Erstes Zweites
By default invalid order values are handled best-effort: a segment with a non-numeric order is sorted after all explicitly ordered segments.
With strictMode(StrictMode.TARGET_ORDER) the order values are validated instead. As soon as one <target/> of a unit declares an order, every segment of that unit needs one, and the values must be integers forming a continuous sequence 1..n without gaps or duplicates (in any document order). Otherwise an XliffMessageSourceRuntimeException is thrown.
order values of a unit |
default | StrictMode.TARGET_ORDER |
|---|---|---|
2, 1 |
sorted 1, 2 |
accepted |
1, 3 (gap) |
sorted 1, 3 |
rejected |
1, 1 (duplicate) |
sorted as written | rejected |
1, abc (non-numeric) |
abc is sorted last |
rejected |
XLIFF 2.2 adds the PGS module. It annotates a <unit/> with a pgs:switch, so its <segment/>s become plural, gender or select cases. Such a unit resolves to different text depending on a runtime argument, for example a count or a gender. This requires ICU4J via enableICU4j() (see MessageSource Configuration).
🚨 See XLIFF 2.2: PGS Module for the annotation, all switch types and examples.
Applies to XLIFF 1.2 and 2.x. The value is the element's text content. Embedded markup (e.g. HTML) is kept verbatim, as a CDATA section or escaped. XLIFF inline elements are reconstructed as plain text: placeholders and codes are replaced by their original data, wrapping elements such as <g/> or <mrk/> keep their text, and annotation markers are dropped.
🚨 See XLIFF Inline Elements for all rules and examples.
Applies to XLIFF 1.2 and 2.x. The value is trimmed by default. Set xml:space="preserve" on the <source/> / <target/> (or an ancestor) to keep leading and trailing whitespace.
<unit id="1" name="separator">
<segment>
<target xml:space="preserve"> · </target>
</segment>
</unit>Result: separator → · (with the surrounding spaces preserved)
There are two ways a file's target locale is determined: by default from the filename suffix, or from the XLIFF document's own language attribute instead if useXliffLanguageAttribute() is enabled on the builder.
The <name> part is freely choosable and has no functional meaning. It is not used to derive keys, and files aren't otherwise linked by it (see XLIFF Files). What matters is that the locale is recognised as a suffix of the filename:
# Default language
<name>.xlf // <name>_<language>.xlf also works.
# Name + Language
<name>[-._]<language>.xlf
# Name + Language + Region
<name>[-._]<language>[-_]<region>.xlf
Enable useXliffLanguageAttribute() on the builder to resolve a file's target locale from the document's own language attribute (target-language for XLIFF 1.2, trgLang for XLIFF 2.x) instead. The filename is then completely free-form. Like the <name> part above, it has no functional meaning at all, so the locale suffix isn't needed either.
return XliffResourceMessageSource
.builder(
Locale.forLanguageTag("en"),
"translations/*"
)
.useXliffLanguageAttribute()
.build();[resources]
|-[translations]
|-a.xliff // target-language="en"
|-b.xliff // target-language="de"
<!-- b.xliff -->
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2"
xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file original="messages"
datatype="plaintext"
source-language="en"
target-language="de">
<body>
<trans-unit id="1" resname="headline">
<source>Headline</source>
<target>Überschrift</target>
</trans-unit>
</body>
</file>
</xliff>For any other strategy, implement TargetLocaleResolverInterface and pass it to targetLocaleResolver(TargetLocaleResolverInterface).
XliffLanguageAttrParser, which implements the XLIFF Language Attribute strategy, is a good example of an implementation.
This package focuses on reading and displaying translations (key → text), not on editing XLIFF with translation tools. Features that only matter for the authoring round-trip are intentionally not processed: a document using them still loads, the features are ignored, and only the resolved text is returned.
Not supported, relative to the XLIFF 1.2 and 2.x specifications (a — means the version has no such concept):
| Feature | XLIFF 1.2 | XLIFF 2.x | Description |
|---|---|---|---|
| Inline element display hint | — | disp |
Only relevant for CAT-tool display; ignored (see XLIFF Inline Elements and Markup). |
| Translation state | state, state-qualifier |
segment state |
<target/> is always used, regardless of state. |
| Notes & alternative translations | <note/>, <alt-trans/> |
<notes/> |
Not exposed. |
| Process metadata | approved, <phase-group/> / phase, tool |
tool / metadata |
Ignored. |
| Skeleton / round-trip structure | <skl/> / external skeleton |
<skeleton/> |
Not read. |
| Grouping & context | <group/>, restype, <context-group/>, <count-group/> |
<group/> |
Structural metadata ignored. |
| Binary content | <bin-unit/>, <bin-source/>, <bin-target/> |
— | Not read. |
| XLIFF 2.x modules | — | Translation Candidates, Glossary, Metadata, Resource Data, Size/Length Restriction, Format Style, Validation, Change Tracking | Not processed (the XLIFF 2.2 PGS module is supported, see XLIFF 2.2: PGS Module). |
A Full Example using Spring Boot, mixing XLIFF 1.2 and XLIFF 2.x translation files:
Repository: https://github.com/alaugks/spring-messagesource-xliff-example
- JSON MessageSource for Spring
- Example: XLIFF MessageSource for Spring
- Example: JSON MessageSource for Spring
- Example: Custom Database Spring MessageSource
Licensed under the Apache License, Version 2.0.