2009-02-05

Multi Language XSLT: Language Texts

Currently I am refactoring the default templates for the upcoming papaya CMS 5 release. I will show you some of the concepts in this blog. As you probably know papaya CMS uses XSLT for its templates, which is imho a perfect choice for web applications.

You get a strict split between application logic and layout. But XSLT can do more. How about translating layout texts, like the caption of a more link, format numbers and dates? Sounds nice, doesn't it?

In the first step you need to separate the layouts texts from the xslt and create language files for easier management.

The template for this is quite small:

<xsl:template name="language-text">
  <xsl:param name="text"></xsl:param>
  <xsl:choose>
    <xsl:when
      test="$LANGUAGE_TEXTS_CURRENT/text[@ident = $text]">
      <xsl:value-of
        select="$LANGUAGE_TEXTS_CURRENT/text[@ident = $text]"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$text"/>
    <xsl:otherwise>    
  </xsl:choose>
</xsl:template>

This will check for an element text in the variable $LANGUAGE_TEXTS_CURRENT with an attribute ident that has the same value like the parameter $text and output it's content.

To fill up the variable, create a xml file with your texts.

<texts>
  <text ident="SAMPLE">Sample text</text>
</texts>

At the top of the XSL file define a global parameter and load a xml file into it. The Xpath function document() loads XML data from an URI. By default the URI is relative to the current xsl file.

<xsl:param name="LANGUAGE_TEXTS_CURRENT"
  select="document('./de-DE.xml')/texts"/>

Of course this whould be only a single fixed language file. So you have to use a variable for the file name. Xpath concat() supports a dynamic count of parameters - no need for nesting.

<xsl:param name="LANGUAGE_TEXTS_CURRENT"
  select="document(concat('./', $PAGE_LANGUAGE, '.xml'))/texts"/>

Now you can call the template to get the language specific text.

<xsl:call-template name="language-text">
  <xsl:with-param name="text">SAMPLE</text>
</xsl:call-template>

This is still a little noisy, but in standard XSLT you can not help it. However if your processor supports the EXSLT extension you can. With EXSLT you can convert a template into a function. The result whould look like this:

<xsl:value-of select="language:text('SAMPLE')" />

Less source and easier to read. You could use it in an output tag, too.

<img src="sample.png" alt="{language:text('SAMPLE')}" />

The PHP 5 ext/xsl using the libxslt library supports EXSLT. To convert the template to a function you change the declaration from "xsl:template" to "func:function" after you did import the EXSLT function namespace:

<func:function name="language:text">
  <xsl:param name="text"/>
  <func:result>
    <xsl:choose>
      <xsl:when
        test="$LANGUAGE_TEXTS_CURRENT/text[@ident = $text]">
        <xsl:value-of
          select="$LANGUAGE_TEXTS_CURRENT/text[@ident = $text]"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$text"/>
      <xsl:otherwise>    
    </xsl:choose>
  </func:result>
</func:function>

Here's still one little problem, if you haven't translated the language xml file to the current language or you missed a phrase the result is the text identifier. It whould be nice to fall back to a default language. So declare an additional parameter and add a condition to the template.

<xsl:param name="LANGUAGE_TEXTS_FALLBACK"
  select="document('./en-US.xml')/texts"/>
<func:function name="language:text">
  <xsl:param name="text"/>
  <func:result>
    <xsl:choose>
      <xsl:when
        test="$LANGUAGE_TEXTS_CURRENT/text[@ident = $text]">
        <xsl:value-of
          select="$LANGUAGE_TEXTS_CURRENT/text[@ident = $text]"/>
      </xsl:when>
      <xsl:when
        test="$LANGUAGE_TEXTS_FALLBACK/text[@ident = $text]">
        <xsl:value-of
          select="$LANGUAGE_TEXTS_FALLBACK/text[@ident = $text]"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$text"/>
      <xsl:otherwise>    
    </xsl:choose>
  </func:result>
</func:function>

You can extend this idea and have default and project specific language files.

Have fun experimenting.

1 comment:

x