Pure XSL localization

Transforming XML data to a human readable presentation of content could be easily done using XSL stylesheet transformations. Even various task could be performed by a standard XSL processor without demanding additional programming language integration. The following example sets up a project structure, needed resource files and transformations to localise the output of an XML transformation by purely using XSLT and XPath.

The objective of this sample project is to provide a localised HTML representation of an XML order. The overall project layout is defined as follows:

Layout of the sample project
Layout of the sample project.

As an example imagine the following data structure describing an order of a customer:

<?xml version="1.0" encoding="UTF-8"?>
<o:order xmlns:o="http://mindcrime-ilab/2012/order-example">
	<o:recipient>
		<o:name>Bar</o:name>
		<o:forename>Foo</o:forename>
		<o:address>
			<o:street>742 Evergreen Terrace</o:street>
			<o:city>Sprinfield</o:city>
		</o:address>
	</o:recipient>
	<o:list>
		<o:item no="1-2334-A" quantity="1">
			<o:name>3.6in High Density Floppy Disk</o:name>
		</o:item>
	</o:list>
</o:order>

In order to derive an invoice for the customer we like to transform this data structure into a human readable version by applying an XSL transformation to it. The transformation will be set up to know where to find the data base of localised strings (resources_de.xml, resources_en.xml, …) and the target language (e.g. ‚de‘).

Import resources and set up localisation

For the application of localized strings there are resource files below the i18n folder. The naming scheme is build from the convention of adding an underscore with the ISO country abbreviation. For example the file containing the German localisation strings is named ‚resources_de.xml‘.

Before applying the transformation we should pass some parameters to our transformation to set up our target environment. The actual customization includes the location of the translation files as well as the target language.

<xsl:param name="resPathPrefix" select="string('../i18n')"></xsl:param>
<xsl:param name="lang" select="string('en')" />

If there is no ‚lang‘ parameter given the English locale is chosen by default.

Load and use strings for localised environment

Loading the localisation strings for a certain environment is fairly easy.

<!-- initialize variable containing all localised strings -->
<xsl:variable name="labels" select="document(concat($resPathPrefix,'/resources_',$lang,'.xml'))/i18n:labels" />

But the behaviour of different XSLT processors differ, some of them will just output an error but keep on processing the transformation instructions whereas others will fail and raise an error. The XSLT specification points out:

If there is an error in processing the fragment identifier, the XSLT processor may signal the error; if it does not signal the error, it must recover by returning an empty node-set.

Even trying to build some kind of file existence check containing a fall back solution for localisation might fail depending on the processor in use. Therefore you better programmatically assure the existence of your localisation files.

<!-- locale defines the file containing the localisation entries -->
<xsl:variable name="locale">
	<!-- test if there is a localisation file for given locale, otherwise use default -->
	<xsl:choose>
		<xsl:when test="boolean(document(concat($resPathPrefix,'/resources_',$lang,'.xml')))">
			<!-- localisation file exists -->
			<xsl:value-of select="concat($resPathPrefix,'/resources_',$lang,'.xml')" />
		</xsl:when>
		<xsl:otherwise>
			<!-- localisation file does not exist; use default eg. English locale -->
			<xsl:value-of select="concat($resPathPrefix,'/resources_en.xml')" />
		</xsl:otherwise>
	</xsl:choose>
</xsl:variable>

<!-- initialize variable containing all localised strings -->
<xsl:variable name="labels" select="document($locale)/i18n:labels" />

In case of requesting a locale where is no localisation available it will use the default locale – in the example above it will fall back to English. This will prevent the transformation from displaying no localisation at all. The first step figures out the existence of the request localisation file and assigns the file reference to the variable ‚locale‘. The ‚locale‘ variable is used to pass the file location to the ‚document‘ function responsible to load the content from the file which is specified by the following XPath expression.

In subsequent instructions we can refer to a localisation string by simply requesting it from the ‚labels‘ variable:

<xsl:template match="o:order">
	<h>
		<xsl:value-of select="$labels/i18n:label[@id='order.recipient']" />
	</h>
	<table style="border:0px;">
		<tr>
			<th>
				<xsl:value-of select="$labels/i18n:label[@id='order.recipient.name']" />
			</th>
			<td>
				<xsl:value-of select="o:recipient/o:name" />
			</td>
		</tr>
		<tr>
			<th>
				<xsl:value-of select="$labels/i18n:label[@id='order.recipient.forename']" />
			</th>
			<td>
				<xsl:value-of select="o:recipient/o:forename" />
			</td>
		</tr>
	</table>
</xsl:template>

The above snippet accesses the localised strings by artless querying them by the id attribute.

Finally here are two examples of a localisation file. Of course you are free to choose any other structural representation meeting the needs of your project:

  • German localisation file:
    <i18n:labels xmlns:i18n="http://mindcrime-ilab/2012/order-example/i18n">
    	<i18n:label id="order.recipient">Empfänger</i18n:label>
    	<i18n:label id="order.recipient.name">Name</i18n:label>
    	<i18n:label id="order.recipient.forename">Vorname</i18n:label>
    </i18n:labels>
    
  • English / default localisation file:
    <i18n:labels xmlns:i18n="http://mindcrime-ilab/2012/order-example/i18n">
    	<i18n:label id="order.recipient">Recipient</i18n:label>
    	<i18n:label id="order.recipient.name">Surname</i18n:label>
    	<i18n:label id="order.recipient.forename">Forename</i18n:label>
    </i18n:labels>
    

Applying the transformation with parameter ‚lang‘ set to ‚de‘ the result will contain the following part:

Empfänger
<table style="border: 0px;">
	<tbody>
		<tr>
			<th>Name</th>
			<td>Bar</td>
		</tr>
		<tr>
			<th>Vorname</th>
			<td>Foo</td>
		</tr>
	</tbody>
</table>

In contrast if you apply the transformation using ‚en‘ as value for the ‚lang‘ parameter the result will contain the following part:

Recipient
<table style="border: 0px;">
	<tbody>
		<tr>
			<th>Surname</th>
			<td>Bar</td>
		</tr>
		<tr>
			<th>Forename</th>
			<td>Foo</td>
		</tr>
	</tbody>
</table>

Non-complete processor test

As pointed out earlier there are functional differences in handling non-existing files by the document function. The following table provides an non-complete, non-representative overview about the feasibility of this approach using different implementations:

Processor Comment
libxslt 10126 Works with fall back, even if for the specified language the localisation file does not exist (tested using xsltproc).
TraX (OpenJDK 7 / 1.7.0_09) Fall back variant does not work. Execution throws an error and will abort if the file does not exist. You better check carefully in your calling implementation for the existence of your localisation files.
Xalan (Java) 2.7.1 Works with fall back, the document function triggers a warning that it could not load the requested document.
Saxon-HE 9.4 Works with fall back, but the document function triggers an I/O error message about the missing file.

Using this approach carefully in respect to the existence of localisation files gives you a powerful tool to stylesheet localisation. I recently used this solution in a project to present localised information to users.

Update(28/12/2012)
Nick Wellnhofer advised me of a posting of him from 2002 Multilingual websites with XSLT and on patent grant in Japan (2003) and the US (2008).

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

19 − 10 =