From 124b153350bcadb0c5287642a38c2566d7632a58 Mon Sep 17 00:00:00 2001 From: resurtm Date: Fri, 10 May 2013 23:57:24 +0600 Subject: [PATCH] Initial Gettext support. --- tests/unit/data/i18n/test.mo | Bin 0 -> 1426 bytes tests/unit/data/i18n/test.po | 64 +++++ .../framework/i18n/GettextMessageSourceTest.php | 14 ++ tests/unit/framework/i18n/GettextMoFileTest.php | 95 ++++++++ tests/unit/framework/i18n/GettextPoFileTest.php | 95 ++++++++ yii/i18n/GettextFile.php | 37 +++ yii/i18n/GettextMessageSource.php | 59 +++++ yii/i18n/GettextMoFile.php | 267 +++++++++++++++++++++ yii/i18n/GettextPoFile.php | 97 ++++++++ 9 files changed, 728 insertions(+) create mode 100644 tests/unit/data/i18n/test.mo create mode 100644 tests/unit/data/i18n/test.po create mode 100644 tests/unit/framework/i18n/GettextMessageSourceTest.php create mode 100644 tests/unit/framework/i18n/GettextMoFileTest.php create mode 100644 tests/unit/framework/i18n/GettextPoFileTest.php create mode 100644 yii/i18n/GettextFile.php create mode 100644 yii/i18n/GettextMessageSource.php create mode 100644 yii/i18n/GettextMoFile.php create mode 100644 yii/i18n/GettextPoFile.php diff --git a/tests/unit/data/i18n/test.mo b/tests/unit/data/i18n/test.mo new file mode 100644 index 0000000000000000000000000000000000000000..d5f94f14a3b52a4ef0b2ddaeeb6bcff474f67594 GIT binary patch literal 1426 zcma)5!EO{s5FL^wht2_sD?t=@X23>B$b=+_kjTQoDlCWtIkab+bw@KjOHYqUq#SIb zzySeL6fPVH+{n#YJg^vhIB-Vl`GNdF{vog0Ya0m;j8t1y-PQG8z3%;YZ|6aT?=#pJ zusPW0uwP;PJ%zo6{SCVdyRj>Z_QCJ|B#QRKADsBF;r~PaBm5iGoc%P42>G7fQFH+Q z2t4QAiN6kyvZwB~v*%^;ULOVT3$ zO3X=|jnA00z^fU|&~d0PuEgI%WV|b;)GZW0gQ?_La!f@2k$>gK?JZxoH~q*r?UJwk zh9CQyeB)OUHhv9@E7(SykpC-`thUQoS;3K4;lM_F$G@bIthLKHzvi15LG9O3wSn4k zdmA9j*cfvq7Gc2lFQ~oGeeeqs=zCxI*W9E19pGHC;h&*k93bCqvrc|Qs042Sp=pDJ z8IOE@h&Up^*XYA&K=SKc-hjy_>&;Pd@bv^~l~It@?Gik;G4LNDza1plJO*_H4tz+R z$AEglq=CUqY6{|UPoAam&x2e+-w`zX2bEBlCkM6m9`Cc%n{s{(`6TiW!@V^)HxV}s z03a4-6$MYA>k{HMzY=5#sw@#E82J_K|A}#bgUF44DFk0#xUm{Hk6Xq+2Ay$r?M?hy NwnF~HY{f^T{{g1ziG%\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" diff --git a/tests/unit/framework/i18n/GettextMessageSourceTest.php b/tests/unit/framework/i18n/GettextMessageSourceTest.php new file mode 100644 index 0000000..7b499f4 --- /dev/null +++ b/tests/unit/framework/i18n/GettextMessageSourceTest.php @@ -0,0 +1,14 @@ +markTestSkipped(); + } +} diff --git a/tests/unit/framework/i18n/GettextMoFileTest.php b/tests/unit/framework/i18n/GettextMoFileTest.php new file mode 100644 index 0000000..0aa22da --- /dev/null +++ b/tests/unit/framework/i18n/GettextMoFileTest.php @@ -0,0 +1,95 @@ +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)); + } +} diff --git a/tests/unit/framework/i18n/GettextPoFileTest.php b/tests/unit/framework/i18n/GettextPoFileTest.php new file mode 100644 index 0000000..8dddb40 --- /dev/null +++ b/tests/unit/framework/i18n/GettextPoFileTest.php @@ -0,0 +1,95 @@ +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)); + } +} diff --git a/yii/i18n/GettextFile.php b/yii/i18n/GettextFile.php new file mode 100644 index 0000000..03eecca --- /dev/null +++ b/yii/i18n/GettextFile.php @@ -0,0 +1,37 @@ + + * @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); +} diff --git a/yii/i18n/GettextMessageSource.php b/yii/i18n/GettextMessageSource.php new file mode 100644 index 0000000..0eb7cb3 --- /dev/null +++ b/yii/i18n/GettextMessageSource.php @@ -0,0 +1,59 @@ +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(); + } + } +} diff --git a/yii/i18n/GettextMoFile.php b/yii/i18n/GettextMoFile.php new file mode 100644 index 0000000..bacba52 --- /dev/null +++ b/yii/i18n/GettextMoFile.php @@ -0,0 +1,267 @@ +. + * 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 + * @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"); + } +} diff --git a/yii/i18n/GettextPoFile.php b/yii/i18n/GettextPoFile.php new file mode 100644 index 0000000..cac075b --- /dev/null +++ b/yii/i18n/GettextPoFile.php @@ -0,0 +1,97 @@ + + * @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+"(.*?(?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); + } +}