Browse Source

Initial Gettext support.

tags/2.0.0-alpha
resurtm 12 years ago
parent
commit
124b153350
  1. BIN
      tests/unit/data/i18n/test.mo
  2. 64
      tests/unit/data/i18n/test.po
  3. 14
      tests/unit/framework/i18n/GettextMessageSourceTest.php
  4. 95
      tests/unit/framework/i18n/GettextMoFileTest.php
  5. 95
      tests/unit/framework/i18n/GettextPoFileTest.php
  6. 37
      yii/i18n/GettextFile.php
  7. 59
      yii/i18n/GettextMessageSource.php
  8. 267
      yii/i18n/GettextMoFile.php
  9. 97
      yii/i18n/GettextPoFile.php

BIN
tests/unit/data/i18n/test.mo

Binary file not shown.

64
tests/unit/data/i18n/test.po

@ -0,0 +1,64 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: resurtm <resurtm@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.5.5\n"
msgctxt "context1"
msgid ""
"Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\n"
"aliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel "
"malesuada.\n"
"Nunc vel sapien nunc, a pretium nulla."
msgstr ""
"Олицетворение однократно. Представленный лексико-семантический анализ "
"является\n"
"психолингвистическим в своей основе, но механизм сочленений полидисперсен. "
"Впечатление\n"
"однократно. Различное расположение выбирает сюжетный механизм сочленений."
msgctxt "context1"
msgid "String number two."
msgstr "Строка номер два."
msgctxt "context2"
msgid ""
"The other\n"
"\n"
"context.\n"
msgstr ""
"Другой\n"
"\n"
"контекст.\n"
msgctxt "context1"
msgid ""
"Missing\n"
"\r\t\"translation."
msgstr ""
msgctxt "context1"
msgid ""
"Nunc vel sapien nunc, a pretium nulla.\n"
"Pellentesque habitant morbi tristique senectus et netus et malesuada fames "
"ac turpis egestas."
msgstr "Короткий перевод."
msgid "contextless"
msgstr ""
msgctxt "context2"
msgid ""
"test1\\ntest2\n"
"\\\n"
"test3"
msgstr ""
"тест1\\nтест2\n"
"\\\n"
"тест3"

14
tests/unit/framework/i18n/GettextMessageSourceTest.php

@ -0,0 +1,14 @@
<?php
namespace yiiunit\framework\i18n;
use yii\i18n\GettextMessageSource;
use yiiunit\TestCase;
class GettextMessageSourceTest extends TestCase
{
public function testLoadMessages()
{
$this->markTestSkipped();
}
}

95
tests/unit/framework/i18n/GettextMoFileTest.php

@ -0,0 +1,95 @@
<?php
namespace yiiunit\framework\i18n;
use yii\i18n\GettextMoFile;
use yiiunit\TestCase;
class GettextMoFileTest extends TestCase
{
public function testLoad()
{
$moFile = new GettextMoFile();
$moFilePath = __DIR__ . '/../../data/i18n/test.mo';
$context1 = $moFile->load($moFilePath, 'context1');
$context2 = $moFile->load($moFilePath, 'context2');
// item count
$this->assertCount(3, $context1);
$this->assertCount(2, $context2);
// original messages
$this->assertArrayNotHasKey("Missing\n\r\t\"translation.", $context1);
$this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1);
$this->assertArrayHasKey("String number two.", $context1);
$this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1);
$this->assertArrayHasKey("The other\n\ncontext.\n", $context2);
$this->assertArrayHasKey("test1\\ntest2\n\\\ntest3", $context2);
// translated messages
$this->assertFalse(in_array("", $context1));
$this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1));
$this->assertTrue(in_array('Строка номер два.', $context1));
$this->assertTrue(in_array('Короткий перевод.', $context1));
$this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2));
$this->assertTrue(in_array("тест1\\nтест2\n\\\nтест3", $context2));
}
public function testSave()
{
// initial data
$s = chr(4);
$messages = array(
'Hello!' => 'Привет!',
"context1{$s}Hello?" => 'Привет?',
'Hello!?' => '',
"context1{$s}Hello!?!" => '',
"context2{$s}\"Quotes\"" => '"Кавычки"',
"context2{$s}\nNew lines\n" => "\nПереносы строк\n",
"context2{$s}\tTabs\t" => "\tТабы\t",
"context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r",
);
// create temporary directory and dump messages
$poFileDirectory = __DIR__ . '/../../runtime/i18n';
if (!is_dir($poFileDirectory)) {
mkdir($poFileDirectory);
}
if (is_file($poFileDirectory . '/test.mo')) {
unlink($poFileDirectory . '/test.mo');
}
$moFile = new GettextMoFile();
$moFile->save($poFileDirectory . '/test.mo', $messages);
// load messages
$context1 = $moFile->load($poFileDirectory . '/test.mo', 'context1');
$context2 = $moFile->load($poFileDirectory . '/test.mo', 'context2');
// context1
$this->assertCount(2, $context1);
$this->assertArrayHasKey('Hello?', $context1);
$this->assertTrue(in_array('Привет?', $context1));
$this->assertArrayHasKey('Hello!?!', $context1);
$this->assertTrue(in_array('', $context1));
// context2
$this->assertCount(4, $context2);
$this->assertArrayHasKey("\"Quotes\"", $context2);
$this->assertTrue(in_array('"Кавычки"', $context2));
$this->assertArrayHasKey("\nNew lines\n", $context2);
$this->assertTrue(in_array("\nПереносы строк\n", $context2));
$this->assertArrayHasKey("\tTabs\t", $context2);
$this->assertTrue(in_array("\tТабы\t", $context2));
$this->assertArrayHasKey("\rCarriage returns\r", $context2);
$this->assertTrue(in_array("\rВозвраты кареток\r", $context2));
}
}

95
tests/unit/framework/i18n/GettextPoFileTest.php

@ -0,0 +1,95 @@
<?php
namespace yiiunit\framework\i18n;
use yii\i18n\GettextPoFile;
use yiiunit\TestCase;
class GettextPoFileTest extends TestCase
{
public function testLoad()
{
$poFile = new GettextPoFile();
$poFilePath = __DIR__ . '/../../data/i18n/test.po';
$context1 = $poFile->load($poFilePath, 'context1');
$context2 = $poFile->load($poFilePath, 'context2');
// item count
$this->assertCount(4, $context1);
$this->assertCount(2, $context2);
// original messages
$this->assertArrayHasKey("Missing\n\r\t\"translation.", $context1);
$this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1);
$this->assertArrayHasKey("String number two.", $context1);
$this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1);
$this->assertArrayHasKey("The other\n\ncontext.\n", $context2);
$this->assertArrayHasKey("test1\\\ntest2\n\\\\\ntest3", $context2);
// translated messages
$this->assertTrue(in_array("", $context1));
$this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1));
$this->assertTrue(in_array('Строка номер два.', $context1));
$this->assertTrue(in_array('Короткий перевод.', $context1));
$this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2));
$this->assertTrue(in_array("тест1\\\nтест2\n\\\\\nтест3", $context2));
}
public function testSave()
{
// initial data
$s = chr(4);
$messages = array(
'Hello!' => 'Привет!',
"context1{$s}Hello?" => 'Привет?',
'Hello!?' => '',
"context1{$s}Hello!?!" => '',
"context2{$s}\"Quotes\"" => '"Кавычки"',
"context2{$s}\nNew lines\n" => "\nПереносы строк\n",
"context2{$s}\tTabs\t" => "\tТабы\t",
"context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r",
);
// create temporary directory and dump messages
$poFileDirectory = __DIR__ . '/../../runtime/i18n';
if (!is_dir($poFileDirectory)) {
mkdir($poFileDirectory);
}
if (is_file($poFileDirectory . '/test.po')) {
unlink($poFileDirectory . '/test.po');
}
$poFile = new GettextPoFile();
$poFile->save($poFileDirectory . '/test.po', $messages);
// load messages
$context1 = $poFile->load($poFileDirectory . '/test.po', 'context1');
$context2 = $poFile->load($poFileDirectory . '/test.po', 'context2');
// context1
$this->assertCount(2, $context1);
$this->assertArrayHasKey('Hello?', $context1);
$this->assertTrue(in_array('Привет?', $context1));
$this->assertArrayHasKey('Hello!?!', $context1);
$this->assertTrue(in_array('', $context1));
// context2
$this->assertCount(4, $context2);
$this->assertArrayHasKey("\"Quotes\"", $context2);
$this->assertTrue(in_array('"Кавычки"', $context2));
$this->assertArrayHasKey("\nNew lines\n", $context2);
$this->assertTrue(in_array("\nПереносы строк\n", $context2));
$this->assertArrayHasKey("\tTabs\t", $context2);
$this->assertTrue(in_array("\tТабы\t", $context2));
$this->assertArrayHasKey("\rCarriage returns\r", $context2);
$this->assertTrue(in_array("\rВозвраты кареток\r", $context2));
}
}

37
yii/i18n/GettextFile.php

@ -0,0 +1,37 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use yii\base\Component;
/**
* GettextFile is the base class for representing a Gettext message file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
abstract class GettextFile extends Component
{
/**
* Loads messages from a file.
* @param string $filePath file path
* @param string $context message context
* @return array message translations. Array keys are source messages and array values are translated messages:
* source message => translated message.
*/
abstract public function load($filePath, $context);
/**
* Saves messages to a file.
* @param string $filePath file path
* @param array $messages message translations. Array keys are source messages and array values are
* translated messages: source message => translated message. Note if the message has a context,
* the message ID must be prefixed with the context with chr(4) as the separator.
*/
abstract public function save($filePath, $messages);
}

59
yii/i18n/GettextMessageSource.php

@ -0,0 +1,59 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
class GettextMessageSource extends MessageSource
{
const MO_FILE_EXT = '.mo';
const PO_FILE_EXT = '.po';
/**
* @var string
*/
public $basePath = '@app/messages';
/**
* @var string
*/
public $catalog = 'messages';
/**
* @var boolean
*/
public $useMoFile = true;
/**
* @var boolean
*/
public $useBigEndian = false;
protected function loadMessages($category, $language)
{
$messageFile = Yii::getAlias($this->basePath) . '/' . $language . '/' . $this->catalog;
if ($this->useMoFile) {
$messageFile .= static::MO_FILE_EXT;
} else {
$messageFile .= static::PO_FILE_EXT;
}
if (is_file($messageFile)) {
if ($this->useMoFile) {
$gettextFile = new GettextMoFile(array('useBigEndian' => $this->useBigEndian));
} else {
$gettextFile = new GettextPoFile();
}
$messages = $gettextFile->load($messageFile, $category);
if (!is_array($messages)) {
$messages = array();
}
return $messages;
} else {
Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__);
return array();
}
}
}

267
yii/i18n/GettextMoFile.php

@ -0,0 +1,267 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use yii\base\Exception;
/**
* GettextMoFile represents an MO Gettext message file.
*
* This class is written by adapting Michael's Gettext_MO class in PEAR.
* Please refer to the following license terms.
*
* Copyright (c) 2004-2005, Michael Wallner <mike@iworks.at>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class GettextMoFile extends GettextFile
{
/**
* @var boolean whether to use big-endian when reading and writing an integer.
*/
public $useBigEndian = false;
/**
* Loads messages from an MO file.
* @param string $filePath file path
* @param string $context message context
* @return array message translations. Array keys are source messages and array values are translated messages:
* source message => translated message.
*/
public function load($filePath, $context)
{
if (false === ($fileHandle = @fopen($filePath, 'rb'))) {
throw new Exception('Unable to read file "' . $filePath . '".');
}
if (false === @flock($fileHandle, LOCK_SH)) {
throw new Exception('Unable to lock file "' . $filePath . '" for reading.');
}
// magic
$array = unpack('c', $this->readBytes($fileHandle, 4));
$magic = current($array);
if ($magic == -34) {
$this->useBigEndian = false;
} elseif ($magic == -107) {
$this->useBigEndian = true;
} else {
throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').');
}
// revision
$revision = $this->readInteger($fileHandle);
if ($revision != 0) {
throw new Exception('Invalid MO file revision: ' . $revision . '.');
}
$count = $this->readInteger($fileHandle);
$sourceOffset = $this->readInteger($fileHandle);
$targetOffset = $this->readInteger($fileHandle);
$sourceLengths = array();
$sourceOffsets = array();
fseek($fileHandle, $sourceOffset);
for ($i = 0; $i < $count; ++$i) {
$sourceLengths[] = $this->readInteger($fileHandle);
$sourceOffsets[] = $this->readInteger($fileHandle);
}
$targetLengths = array();
$targetOffsets = array();
fseek($fileHandle, $targetOffset);
for ($i = 0; $i < $count; ++$i) {
$targetLengths[] = $this->readInteger($fileHandle);
$targetOffsets[] = $this->readInteger($fileHandle);
}
$messages = array();
for ($i = 0; $i < $count; ++$i) {
$id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]);
$separatorPosition = strpos($id, chr(4));
if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) ||
(!$context && $separatorPosition === false)) {
if ($separatorPosition !== false) {
$id = substr($id,$separatorPosition+1);
}
$message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]);
$messages[$id] = $message;
}
}
@flock($fileHandle, LOCK_UN);
@fclose($fileHandle);
return $messages;
}
/**
* Saves messages to an MO file.
* @param string $filePath file path
* @param array $messages message translations. Array keys are source messages and array values are
* translated messages: source message => translated message. Note if the message has a context,
* the message ID must be prefixed with the context with chr(4) as the separator.
*/
public function save($filePath, $messages)
{
if (false === ($fileHandle = @fopen($filePath, 'wb'))) {
throw new Exception('Unable to write file "' . $filePath . '".');
}
if (false === @flock($fileHandle, LOCK_EX)) {
throw new Exception('Unable to lock file "' . $filePath . '" for reading.');
}
// magic
if ($this->useBigEndian) {
$this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107
} else {
$this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34
}
// revision
$this->writeInteger($fileHandle, 0);
// message count
$messageCount = count($messages);
$this->writeInteger($fileHandle, $messageCount);
// offset of source message table
$offset = 28;
$this->writeInteger($fileHandle, $offset);
$offset += $messageCount * 8;
$this->writeInteger($fileHandle, $offset);
// hashtable size, omitted
$this->writeInteger($fileHandle, 0);
$offset += $messageCount * 8;
$this->writeInteger($fileHandle, $offset);
// length and offsets for source messages
foreach (array_keys($messages) as $id) {
$length = strlen($id);
$this->writeInteger($fileHandle, $length);
$this->writeInteger($fileHandle, $offset);
$offset += $length + 1;
}
// length and offsets for target messages
foreach ($messages as $message) {
$length = strlen($message);
$this->writeInteger($fileHandle, $length);
$this->writeInteger($fileHandle, $offset);
$offset += $length + 1;
}
// source messages
foreach (array_keys($messages) as $id) {
$this->writeString($fileHandle, $id);
}
// target messages
foreach ($messages as $message) {
$this->writeString($fileHandle, $message);
}
@flock($fileHandle, LOCK_UN);
@fclose($fileHandle);
}
/**
* Reads one or several bytes.
* @param resource $fileHandle to read from
* @param integer $byteCount to be read
* @return string bytes
*/
protected function readBytes($fileHandle, $byteCount = 1)
{
if ($byteCount > 0) {
return fread($fileHandle, $byteCount);
}
}
/**
* Write bytes.
* @param resource $fileHandle to write to
* @param string $bytes to be written
* @return integer how many bytes are written
*/
protected function writeBytes($fileHandle, $bytes)
{
return fwrite($fileHandle, $bytes);
}
/**
* Reads a 4-byte integer.
* @param resource $fileHandle to read from
* @return integer the result
*/
protected function readInteger($fileHandle)
{
$array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4));
return current($array);
}
/**
* Writes a 4-byte integer.
* @param resource $fileHandle to write to
* @param integer $integer to be written
* @return integer how many bytes are written
*/
protected function writeInteger($fileHandle, $integer)
{
return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int)$integer));
}
/**
* Reads a string.
* @param resource $fileHandle file handle
* @param integer $length of the string
* @param integer $offset of the string in the file. If null, it reads from the current position.
* @return string the result
*/
protected function readString($fileHandle, $length, $offset = null)
{
if ($offset !== null) {
fseek($fileHandle, $offset);
}
return $this->readBytes($fileHandle, $length);
}
/**
* Writes a string.
* @param resource $fileHandle to write to
* @param string $string to be written
* @return integer how many bytes are written
*/
protected function writeString($fileHandle, $string)
{
return $this->writeBytes($fileHandle, $string. "\0");
}
}

97
yii/i18n/GettextPoFile.php

@ -0,0 +1,97 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n;
use Yii;
/**
* GettextPoFile represents a PO Gettext message file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class GettextPoFile extends GettextFile
{
/**
* Loads messages from a PO file.
* @param string $filePath file path
* @param string $context message context
* @return array message translations. Array keys are source messages and array values are translated messages:
* source message => translated message.
*/
public function load($filePath, $context)
{
$pattern = '/(msgctxt\s+"(.*?(?<!\\\\))")?\s+' // context
. 'msgid\s+((?:".*(?<!\\\\)"\s*)+)\s+' // message ID, i.e. original string
. 'msgstr\s+((?:".*(?<!\\\\)"\s*)+)/'; // translated string
$content = file_get_contents($filePath);
$matches = array();
$matchCount = preg_match_all($pattern, $content, $matches);
$messages = array();
for ($i = 0; $i < $matchCount; ++$i) {
if ($matches[2][$i] == $context) {
$id = $this->decode($matches[3][$i]);
$message = $this->decode($matches[4][$i]);
$messages[$id] = $message;
}
}
return $messages;
}
/**
* Saves messages to a PO file.
* @param string $filePath file path
* @param array $messages message translations. Array keys are source messages and array values are
* translated messages: source message => translated message. Note if the message has a context,
* the message ID must be prefixed with the context with chr(4) as the separator.
*/
public function save($filePath, $messages)
{
$content = '';
foreach ($messages as $id => $message) {
$separatorPosition = strpos($id, chr(4));
if ($separatorPosition !== false) {
$content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n";
$id = substr($id, $separatorPosition + 1);
}
$content .= 'msgid "' . $this->encode($id) . "\"\n";
$content .= 'msgstr "' . $this->encode($message) . "\"\n\n";
}
file_put_contents($filePath, $content);
}
/**
* Encodes special characters in a message.
* @param string $string message to be encoded
* @return string the encoded message
*/
protected function encode($string)
{
return str_replace(
array('"', "\n", "\t", "\r"),
array('\\"', '\\n', '\\t', '\\r'),
$string
);
}
/**
* Decodes special characters in a message.
* @param string $string message to be decoded
* @return string the decoded message
*/
protected function decode($string)
{
$string = preg_replace(
array('/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'),
array('', "\n", "\r", "\t", '"'),
$string
);
return substr(rtrim($string), 1, -1);
}
}
Loading…
Cancel
Save