Search code examples
xsltxslt-1.0translationlookup

XSLT - Translate strings using key() lookup


I'm trying to solve a problem, where I have to translate strings using xslt.

I saw this: XSLT key() lookup and this: XSLT Conditional Lookup Table

but I'm not able to get it to work. I've tried to come up with the minimal example below which shows the problems that I'm facing. The "real" xsl is assembled from code snippets using a build process. This involves some constraints.

The inner structure of the translation lookup tables always is the same, since they are downloaded from a translation tool in flat xml format http://docs.translatehouse.org/projects/translate-toolkit/en/latest/formats/flatxml.html. I can only wrap them into distinct parent nodes which is what i tried using the "lu" namespace. The translation tables for all languages have to be stored inside the xsl, because different generations of xsl with different translations may exist next to each other. So no "sidecar" files.

Until now I can't get the key to work. The output of xsltproc is the following:

Setup Key - Start
German
xsltApplyOneTemplate: key was not compiled
Setup Key - End
de # skipped #
de # failed #

Expected output:

Setup Key - Start
German
Setup Key - End
de # skipped # Übersprungen
de # failed # Fehlgeschlagen

The XML file just needs to contain a root element.

So obviously the way I try to define the key depending on the target language is wrong, but my xsl knowledge has reached its limit now. The language stays the same during the transformation, so the key for all translation lookups has to be set up only once at the beginning.

The xsl Transformation:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
  xmlns:lu="http://www.my.domain.de/lookup"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:variable name="vLanguageCode">
    <!-- <xsl:value-of select="/root/@language"/> -->
    <xsl:value-of select="'de'"/>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:call-template name="setupKey"/>

    <xsl:call-template name="getLabel">
      <xsl:with-param name="pKey" select="'skipped'"/>
    </xsl:call-template>

    <xsl:call-template name="getLabel">
      <xsl:with-param name="pKey" select="'failed'"/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="setupKey">
    <xsl:message>Setup Key - Start</xsl:message>
    <xsl:choose>
      <xsl:when test="$vLanguageCode='DE' or $vLanguageCode='de'">
        <xsl:message>German</xsl:message>
        <xsl:key name="kLanguageDict" match="/lu:de/root/str" use="@key"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:message>English (default)</xsl:message>
        <xsl:key name="kLanguageDict" match="/lu:en/root/str" use="@key"/>
      </xsl:otherwise>
    </xsl:choose>
    <xsl:message>Setup Key - End</xsl:message>
  </xsl:template>

  <xsl:template name="getLabel">
    <xsl:param name="pKey"/>
    <xsl:variable name="vResult">
      <xsl:value-of select="key('kLanguageDict', $pKey)/@str"/>
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="$vResult!=''">
        <xsl:value-of select="$vResult"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$pKey"/>
      </xsl:otherwise>
    </xsl:choose>
    <xsl:message>
      <xsl:value-of select="concat($vLanguageCode, ' # ', $pKey, ' # ', $vResult)"/>
    </xsl:message>
  </xsl:template>


  <lu:de>
    <root>
      <str key="skipped">Übersprungen</str>
      <str key="failed">Fehlgeschlagen</str>
    </root>
  </lu:de>

  <lu:en>
    <root>
      <str key="skipped">Skipped</str>
      <str key="failed">Failed</str>
    </root>
  </lu:en>

</xsl:stylesheet>

Additions in response to the answer from @michael.hor257k:

  1. Thank you. I didn't know that. So this means that I can't selectively define a key depending on language?
  2. The translation system originally has one key at the top level and a translation table with interleaved entries for each language. It uses a double index (language+id) to look up the values.

I am trying to find a solution where I can embed the xml files returned by the translation management system (weblate) directly into the xsl without having to modify them. Unfortunately it looks like I'm limited in what I can get back (only default nodes and attributes).

This is the core of the original working translation lookup code:


  <xsl:variable name="vLanguageDict" select="document('')/*/lu:strings"/>
  <xsl:key name="kLanguageDict" match="lu:string" use="concat(@lang,@id)"/>

  <xsl:template name="getLabel">
    <xsl:param name="pKey"/>
    <xsl:variable name="vResult">
      <xsl:for-each select="$vLanguageDict">
        <xsl:value-of select="key('kLanguageDict', concat($vLanguageCode,$pKey))/@value" />
      </xsl:for-each>
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="$vResult!=''">
        <xsl:value-of select="$vResult"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$pKey"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

<lu:strings>
  <lu:string lang="DE" id="skipped" value="Übersprungen"/>
  <lu:string lang="EN" id="skipped" value="skipped"/>
  <lu:string lang="DE" id="failed" value="Fehlgeschlagen"/>
  <lu:string lang="EN" id="failed" value="failed"/>
</lu:strings>


Solution

  • There are two mistakes in your XSLT stylesheet that immediately jump out:

    1. The xsl:key element is allowed only at the top level, as a child of the xsl:stylesheet element.
    2. In XSLT 1.0, keys operate only on the current document. If you want to lookup from the stylesheet itself, you must change the context to the stylesheet document before calling the key() function. Here are two examples: https://stackoverflow.com/a/32440143/3016153
      https://stackoverflow.com/a/30188334/3016153

    I am afraid that's about all that can be said without a reproducible example.

    --- added ---

    So this means that I can't selectively define a key depending on language?

    You cannot define a key conditionally - but you can define more than one key and select the one to use based on the specified language. Here's a simplified example:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:dict="http://example.com/dict">
    <xsl:output method="text" encoding="UTF-8"/>
    
    <xsl:key name="de" match="dict:de/entry" use="@key" />
    <xsl:key name="en" match="dict:en/entry" use="@key" />
    
    <xsl:param name="input">skipped</xsl:param>
    <xsl:param name="lang">de</xsl:param>
    
    <xsl:template match="/">
        <xsl:value-of select="$lang"/>
        <xsl:text> # </xsl:text>
        <xsl:value-of select="$input"/>
        <xsl:text> = </xsl:text>
        <!-- switch context to stylesheet in order to use key -->
        <xsl:for-each select="document('')">
            <xsl:value-of select="key($lang, $input)"/>
        </xsl:for-each>
    </xsl:template> 
        
    <dict:de>
        <entry key="skipped">Übersprungen</entry>
        <entry key="failed">Fehlgeschlagen</entry>
    </dict:de>
    
    <dict:en>
        <entry key="skipped">Skipped</entry>
        <entry key="failed">Failed</entry>
    </dict:en>
    
    </xsl:stylesheet>
    

    Applied to any XML input, this will return:

    Result

    de : skipped = Übersprungen