Skip to content

About

Provides a Spring MessageSource for XLIFF files. The package support XLIFF versions 1.2, 2.0, 2.1 and 2.2 (include PGS Module).

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

150 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

XLIFF MessageSource for Spring

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.

Quality Gate Status Maven Central

Table of Contents

How it works

The XLIFF files are read once at startup and held in memory. Message resolution follows Spring's ResourceBundleMessageSource.

  1. Loading: build() reads all matching XLIFF files and turns every unit into a locale / code / value entry, grouped per locale. If a code occurs more than once for the same locale, the first one wins.
  2. Locale fallback: For each requested locale, a java.util.ResourceBundle is created on top of these entries. The standard ResourceBundle lookup 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

Maven

<dependency>
    <groupId>io.github.alaugks</groupId>
    <artifactId>spring-messagesource-xliff</artifactId>
    <version>4.2.0</version>
</dependency>

Gradle

implementation group: 'io.github.alaugks', name: 'spring-messagesource-xliff', version: '4.2.0'

MessageSource Configuration

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.

Example

Java Configuration

  • 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();
    }

}

XLIFF Example Files

  • Default locale is en without region.
  • Translations are provided for the locale en, de and en-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.

messages.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">
        <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>

messages_de.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>

messages_en-US.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>

Target value

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). The id does not exist in en-US, so it tries to select the translation with locale en.

**There is no translation for Japanese (jp). The default locale translations (en) are selected.

XLIFF Files

  • 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, or id if neither is set.
  • Files are selected by locationPattern and by file extension: xlf and xliff by default, configurable with fileExtensions().
  • Supported versions: 1.2, 2.0, 2.1 and 2.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.xsd with the metadata.xsd module). It is off by default and enabled with enableSchemaValidation(). 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, with id as fallback), never the <source/> text. See Translation Key and Translation Value.

Translation Key

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. resname is the original resource name (e.g. a properties-file key) and is preferred; id is required and unique within the <file/> but is a tool-internal identifier, used as the key only when resname is absent. (Docs: General Identifiers)
  • XLIFF 2.x: unit/@name → unit/@id, analogous to 1.2. segment/@id is 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: no name/id).

Translation Value

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).

XLIFF 1.2

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

XLIFF 2.x: Segmentation

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.

XLIFF 2.x: Segments Order

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: PGS Module (Plural, Gender and Select)

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.

XLIFF Inline Elements and Markup

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.

Whitespace

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"> &#183; </target>
    </segment>
</unit>

Result: separator → · (with the surrounding spaces preserved)

Determining the Target Locale

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.

Filename Suffix

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

XLIFF Language Attribute

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>

Custom Target Locale Resolver

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.

Unsupported XLIFF Features

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).

Full Example

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

Related MessageSources and Examples

License

Licensed under the Apache License, Version 2.0.

About

Provides a Spring MessageSource for XLIFF files. The package support XLIFF versions 1.2, 2.0, 2.1 and 2.2 (include PGS Module).

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages