Локализация XSL, PHP и JavaScript из одного XML источника

Изящный способ минимальными средствами сделать полную локализацию веб-приложения.

В проекте WikiCrowd нам понадобилось сделать локализацию. Мы используем XSL для генерации UI, backend написан на PHP и еще есть много JavScript'а. И на всех уровнях нужно выводить локализованный текст.

XSL

С XSL все достаточно просто. Очень хорошее описание локализации XSL есть на сайте Студии Лебедева. Для каждого языка получается отдельный XML файл:

<locale language="English">
<message id="AccessRights" text="New user rights"/>
...

Который подключается к XSL-шаблону:

<xsl:variable name="locale" select="document(concat('locale/', $LOCALE, '.xml'))/locale"/>

Локализованную строку подставляем так:

<xsl:value-of select="$locale//message[@id='Logout']/@text"/>

Его же мы решили использовать для локализации PHP и JavaScript компонент.

PHP

В PHP мы решили сделать из локализованных строк константы в отдельном namespace (чтобы случайно не пересечься) и определили функцию для обращения к ним:

$dom = new DOMDocument();
$dom->load('xml/locale/'.LOCALE.'.xml');
$messages = $dom->getElementsByTagName('message');
for($i = 0; $i < $messages->length; $i++) {
    $message = $messages->item($i);
    define('locale\\'.       $message->getAttribute('id'), $message->getAttribute('text'));
}

function getMessage($id) {
    return str_replace('\\n', "\n", constant('locale\\'.$id));
}

Там, где нужна локализованная строка, используем вызов getMessage('Configure').

JavaScript

С JavaScript оказалось все немного сложнее, потому что понадобилось локализовать не только статические строки, но и алгоритм :). Для удобства чтения мы выводим дату в формате "изменение произошло 3 часа 25 минут назад", и простой локализацией "часов" и "минут" оказалось недостаточно, потому что в русском языке алгоритм определения окончания не такой тривиальный, как, например, в английском (1 minute, 2+ minutes).

Так в XML-файле с локализованными строками появились локализованные функции:

<function id="getDaysText" params="days"><![CDATA[return days + " day" + (days > 1 ? "s" : "");]]></function>

Из локализованных строк и функций с помощью XSL генерируется исходник на JavaScript:

<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:php="http://php.net/xsl"
  exclude-result-prefixes="">
  <xsl:output
    method="text"
    version="1.0"
    indent="no"
    encoding="utf-8"
    omit-xml-declaration="yes"
    media-type="text/plain"
    cdata-section-elements=""/>

  <xsl:template match="/">
var Locale = {
<xsl:apply-templates select="//message"/>
<xsl:apply-templates select="//function"/>
  getMessage: function(id) {
    var text = this[id];
    for(var i = 1; i <xsl:text disable-output-escaping="yes"><</xsl:text> arguments.length; i++)
      text = text.replace('%' + i, arguments[i]);
    return text;
  }
};
  </xsl:template>

  <xsl:template match="message">
    <xsl:variable name="ap">'</xsl:variable>
<xsl:value-of select="@id"/>: '<xsl:value-of select="php:functionString('jsStringReplace', @text)" disable-output-escaping="yes"/>',
</xsl:template>
  <xsl:template match="function">
<xsl:value-of select="@id"/>: function(<xsl:value-of select="@params"/>) {
<xsl:value-of select="text()" disable-output-escaping="yes"/>
},
</xsl:template>

</xsl:stylesheet>

Как вы могли заметить, мы используем в PHP-расширение, позволяющее вызывать функции PHP из XSL-шаблона. Функция jsStringReplace() заменяет некоторые символы, чтобы JavaScript-код получится правильным:

function jsStringReplace($str) {
  return strtr($str, array('\''=>'\\\'', '&lt;'=>'<', '&gt;'=>'>'));
}

Обращение к локализованной строке выглядит так: Locale.Change.

До сих пор этот вариант локализации работает отлично.

Комментариев нет: