diff --git a/src/Symfony/Component/Intl/Exception/UnexpectedTypeException.php b/src/Symfony/Component/Intl/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000000..645d4926da --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/UnexpectedTypeException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Thrown when a method argument had an unexpected type. + * + * @author Bernhard Schussek + */ +class UnexpectedTypeException extends InvalidArgumentException +{ + public function __construct($value, $expectedType) + { + parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value))); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php b/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php index c69bec62dc..2aa702089f 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php @@ -11,11 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Writer; +use Symfony\Component\Intl\Exception\UnexpectedTypeException; + /** * Writes .txt resource bundles. * - * The resulting files can be converted to binary .res files using the - * {@link \Symfony\Component\Intl\ResourceBundle\Transformer\BundleCompiler}. + * The resulting files can be converted to binary .res files using a + * {@link \Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompilerInterface} + * implementation. * * @author Bernhard Schussek * @@ -28,11 +31,11 @@ class TextBundleWriter implements BundleWriterInterface /** * {@inheritdoc} */ - public function write($path, $locale, $data) + public function write($path, $locale, $data, $fallback = true) { $file = fopen($path.'/'.$locale.'.txt', 'w'); - $this->writeResourceBundle($file, $locale, $data); + $this->writeResourceBundle($file, $locale, $data, $fallback); fclose($file); } @@ -43,14 +46,16 @@ class TextBundleWriter implements BundleWriterInterface * @param resource $file The file handle to write to. * @param string $bundleName The name of the bundle. * @param mixed $value The value of the node. + * @param bool $fallback Whether the resource bundle should be merged + * with the fallback locale. * * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt */ - private function writeResourceBundle($file, $bundleName, $value) + private function writeResourceBundle($file, $bundleName, $value, $fallback) { fwrite($file, $bundleName); - $this->writeTable($file, $value, 0); + $this->writeTable($file, $value, 0, $fallback); fwrite($file, "\n"); } @@ -74,16 +79,25 @@ class TextBundleWriter implements BundleWriterInterface return; } + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + if (is_array($value)) { - if (count($value) === count(array_filter($value, 'is_int'))) { + $intValues = count($value) === count(array_filter($value, 'is_int')); + + $keys = array_keys($value); + + // check that the keys are 0-indexed and ascending + $intKeys = $keys === range(0, count($keys) - 1); + + if ($intValues && $intKeys) { $this->writeIntVector($file, $value, $indentation); return; } - $keys = array_keys($value); - - if (count($keys) === count(array_filter($keys, 'is_int'))) { + if ($intKeys) { $this->writeArray($file, $value, $indentation); return; @@ -182,16 +196,35 @@ class TextBundleWriter implements BundleWriterInterface /** * Writes a "table" node. * - * @param resource $file The file handle to write to. - * @param array $value The value of the node. - * @param int $indentation The number of levels to indent. + * @param resource $file The file handle to write to. + * @param array|\Traversable $value The value of the node. + * @param int $indentation The number of levels to indent. + * @param bool $fallback Whether the table should be merged + * with the fallback locale. + * + * @throws UnexpectedTypeException When $value is not an array and not a + * \Traversable instance. */ - private function writeTable($file, array $value, $indentation) + private function writeTable($file, $value, $indentation, $fallback = true) { + if (!is_array($value) && !$value instanceof \Traversable) { + throw new UnexpectedTypeException($value, 'array or \Traversable'); + } + + if (!$fallback) { + fwrite($file, ":table(nofallback)"); + } + fwrite($file, "{\n"); foreach ($value as $key => $entry) { fwrite($file, str_repeat(' ', $indentation + 1)); + + // escape colons, otherwise they are interpreted as resource types + if (false !== strpos($key, ':') || false !== strpos($key, ' ')) { + $key = '"'.$key.'"'; + } + fwrite($file, $key); $this->writeResource($file, $entry, $indentation + 1); diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt index 0ee0d7f2f5..09c1275fa5 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt @@ -14,6 +14,22 @@ en{ 2, 3, } + NotAnIntVector{ + 0:int{0} + 2:int{1} + 1:int{2} + 3:int{3} + } + IntVectorWithStringKeys{ + a:int{0} + b:int{1} + c:int{2} + } + TableWithIntKeys{ + 0:int{0} + 1:int{1} + 3:int{3} + } FalseBoolean{"false"} TrueBoolean{"true"} Null{""} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en_nofallback.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en_nofallback.txt new file mode 100644 index 0000000000..85386f2074 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en_nofallback.txt @@ -0,0 +1,3 @@ +en_nofallback:table(nofallback){ + Entry{"Value"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/escaped.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/escaped.txt new file mode 100644 index 0000000000..6669bfdd83 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/escaped.txt @@ -0,0 +1,4 @@ +escaped{ + "EntryWith:Colon"{"Value"} + "Entry With Spaces"{"Value"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php index cbe0c8d8bf..f42b2738d7 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php @@ -36,7 +36,7 @@ class TextBundleWriterTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->writer = new TextBundleWriter(); - $this->directory = sys_get_temp_dir() . '/TextBundleWriterTest/' . rand(1000, 9999); + $this->directory = sys_get_temp_dir().'/TextBundleWriterTest/'.rand(1000, 9999); $this->filesystem = new Filesystem(); $this->filesystem->mkdir($this->directory); @@ -54,6 +54,9 @@ class TextBundleWriterTest extends \PHPUnit_Framework_TestCase 'Array' => array('foo', 'bar', array('Key' => 'value')), 'Integer' => 5, 'IntVector' => array(0, 1, 2, 3), + 'NotAnIntVector' => array(0 => 0, 2 => 1, 1 => 2, 3 => 3), + 'IntVectorWithStringKeys' => array('a' => 0, 'b' => 1, 'c' => 2), + 'TableWithIntKeys' => array(0 => 0, 1 => 1, 3 => 3), 'FalseBoolean' => false, 'TrueBoolean' => true, 'Null' => null, @@ -62,6 +65,51 @@ class TextBundleWriterTest extends \PHPUnit_Framework_TestCase 'Entry2' => 'String', )); - $this->assertFileEquals(__DIR__ . '/Fixtures/en.txt', $this->directory . '/en.txt'); + $this->assertFileEquals(__DIR__.'/Fixtures/en.txt', $this->directory.'/en.txt'); + } + + public function testWriteTraversable() + { + $this->writer->write($this->directory, 'en', new \ArrayIterator(array( + 'Entry1' => new \ArrayIterator(array( + 'Array' => array('foo', 'bar', array('Key' => 'value')), + 'Integer' => 5, + 'IntVector' => array(0, 1, 2, 3), + 'NotAnIntVector' => array(0 => 0, 2 => 1, 1 => 2, 3 => 3), + 'IntVectorWithStringKeys' => array('a' => 0, 'b' => 1, 'c' => 2), + 'TableWithIntKeys' => array(0 => 0, 1 => 1, 3 => 3), + 'FalseBoolean' => false, + 'TrueBoolean' => true, + 'Null' => null, + 'Float' => 1.23, + )), + 'Entry2' => 'String', + ))); + + $this->assertFileEquals(__DIR__.'/Fixtures/en.txt', $this->directory.'/en.txt'); + } + + public function testWriteNoFallback() + { + $data = array( + 'Entry' => 'Value' + ); + + $this->writer->write($this->directory, 'en_nofallback', $data, $fallback = false); + + $this->assertFileEquals(__DIR__.'/Fixtures/en_nofallback.txt', $this->directory.'/en_nofallback.txt'); + } + + public function testEscapeKeysIfNecessary() + { + $this->writer->write($this->directory, 'escaped', array( + // Keys with colons must be escaped, otherwise the part after the + // colon is interpreted as resource type + 'EntryWith:Colon' => 'Value', + // Keys with spaces must be escaped + 'Entry With Spaces' => 'Value', + )); + + $this->assertFileEquals(__DIR__.'/Fixtures/escaped.txt', $this->directory.'/escaped.txt'); } }