Browse Source

Merge branch 'master' into 3.next

Mark Story 8 years ago
parent
commit
27afeaef62

+ 2 - 2
.travis.yml

@@ -32,7 +32,7 @@ cache:
 
 matrix:
   fast_finish: true
-  
+
   allow_failures:
     - php: nightly
 
@@ -75,7 +75,7 @@ script:
   - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION != 7.0 ]]; then vendor/bin/phpunit; fi
 
   - if [[ $PHPCS = 1 ]]; then composer cs-check; fi
-  - if [[ $PHPSTAN = 1 ]]; then composer require --dev phpstan/phpstan:^0.6 && vendor/bin/phpstan analyse -c phpstan.neon -l 1 src; fi
+  - if [[ $PHPSTAN = 1 ]]; then composer require --dev phpstan/phpstan:^0.7 && vendor/bin/phpstan analyse -c phpstan.neon -l 1 src; fi
 
 after_success:
   - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.0 ]]; then bash <(curl -s https://codecov.io/bash); fi

+ 1 - 1
VERSION.txt

@@ -16,4 +16,4 @@
 // @license       https://opensource.org/licenses/mit-license.php MIT License
 // +--------------------------------------------------------------------------------------------+ //
 ////////////////////////////////////////////////////////////////////////////////////////////////////
-3.4.10
+3.4.11

+ 3 - 1
phpstan.neon

@@ -1,6 +1,6 @@
 parameters:
     autoload_files:
-        - %rootDir%/../../../src/basics.php
+        - %rootDir%/../../../tests/bootstrap.php
     ignoreErrors:
         - '#Function wincache_ucache_[a-zA-Z0-9_]+ not found#'
         - '#Function xcache_[a-zA-Z0-9_]+ not found#'
@@ -15,6 +15,8 @@ parameters:
         - '#Access to an undefined property Cake\\Http\\Client\\Message::\$headers#'
         - '#Access to an undefined property Cake\\[a-zA-Z0-9_\\]+::\$_validViewOptions#'
         - '#Access to an undefined property Cake\\Database\\Driver::\$_connection#'
+        - '#Constant XC_TYPE_VAR not found#'
+        - '#Call to static method id\(\) on an unknown class PHPUnit_Runner_Version#'
     earlyTerminatingMethodCalls:
         Cake\Shell\Shell:
             - abort

+ 2 - 2
src/Collection/CollectionInterface.php

@@ -451,8 +451,8 @@ interface CollectionInterface extends Iterator, JsonSerializable
      *
      * ```
      * $items = [
-     *  ['invoice' => ['total' => 100],
-     *  ['invoice' => ['total' => 200]
+     *  ['invoice' => ['total' => 100]],
+     *  ['invoice' => ['total' => 200]]
      * ];
      *
      * $total = (new Collection($items))->sumOf('invoice.total');

+ 36 - 4
src/Console/Shell.php

@@ -702,7 +702,15 @@ class Shell
      */
     public function err($message = null, $newlines = 1)
     {
-        return $this->_io->err('<error>' . $message . '</error>', $newlines);
+        if (is_array($message)) {
+            foreach ($message as $k => $v) {
+                $message[$k] = '<error>' . $v . '</error>';
+            }
+        } else {
+            $message = '<error>' . $message . '</error>';
+        }
+
+        return $this->_io->err($message, $newlines);
     }
 
     /**
@@ -716,7 +724,15 @@ class Shell
      */
     public function info($message = null, $newlines = 1, $level = Shell::NORMAL)
     {
-        return $this->out('<info>' . $message . '</info>', $newlines, $level);
+        if (is_array($message)) {
+            foreach ($message as $k => $v) {
+                $message[$k] = '<info>' . $v . '</info>';
+            }
+        } else {
+            $message = '<info>' . $message . '</info>';
+        }
+
+        return $this->out($message, $newlines, $level);
     }
 
     /**
@@ -729,7 +745,15 @@ class Shell
      */
     public function warn($message = null, $newlines = 1)
     {
-        return $this->_io->err('<warning>' . $message . '</warning>', $newlines);
+        if (is_array($message)) {
+            foreach ($message as $k => $v) {
+                $message[$k] = '<warning>' . $v . '</warning>';
+            }
+        } else {
+            $message = '<warning>' . $message . '</warning>';
+        }
+
+        return $this->_io->err($message, $newlines);
     }
 
     /**
@@ -743,7 +767,15 @@ class Shell
      */
     public function success($message = null, $newlines = 1, $level = Shell::NORMAL)
     {
-        return $this->out('<success>' . $message . '</success>', $newlines, $level);
+        if (is_array($message)) {
+            foreach ($message as $k => $v) {
+                $message[$k] = '<success>' . $v . '</success>';
+            }
+        } else {
+            $message = '<success>' . $message . '</success>';
+        }
+
+        return $this->out($message, $newlines, $level);
     }
 
     /**

+ 3 - 2
src/Http/Client/Adapter/Stream.php

@@ -16,6 +16,7 @@ namespace Cake\Http\Client\Adapter;
 use Cake\Core\Exception\Exception;
 use Cake\Http\Client\Request;
 use Cake\Http\Client\Response;
+use Cake\Network\Exception\HttpException;
 
 /**
  * Implements sending Cake\Http\Client\Request
@@ -238,7 +239,7 @@ class Stream
      *
      * @param \Cake\Http\Client\Request $request The request object.
      * @return array Array of populated Response objects
-     * @throws \Cake\Core\Exception\Exception
+     * @throws \Cake\Network\Exception\HttpException
      */
     protected function _send(Request $request)
     {
@@ -269,7 +270,7 @@ class Stream
         fclose($this->_stream);
 
         if ($timedOut) {
-            throw new Exception('Connection timed out ' . $url);
+            throw new HttpException('Connection timed out ' . $url, 504);
         }
 
         $headers = $meta['wrapper_data'];

+ 1 - 0
src/Http/Client/Response.php

@@ -163,6 +163,7 @@ class Response extends Message implements ResponseInterface
         }
         $stream = new Stream('php://memory', 'wb+');
         $stream->write($body);
+        $stream->rewind();
         $this->stream = $stream;
     }
 

+ 1 - 1
src/Http/ServerRequest.php

@@ -970,7 +970,7 @@ class ServerRequest implements ArrayAccess, ServerRequestInterface
      *
      * @param string $name Name of the header you want.
      * @return string|null Either null on no header being set or the value of the header.
-     * @deprecated 4.0.0 The automatic fallback to env() will be removed in 4.0.0
+     * @deprecated 4.0.0 The automatic fallback to env() will be removed in 4.0.0, see getHeader()
      */
     public function header($name)
     {

+ 1 - 21
src/I18n/Formatter/IcuFormatter.php

@@ -30,10 +30,6 @@ class IcuFormatter implements FormatterInterface
      * Returns a string with all passed variables interpolated into the original
      * message. Variables are interpolated using the MessageFormatter class.
      *
-     * If an array is passed in `$message`, it will trigger the plural selection
-     * routine. Plural forms are selected depending on the locale and the `_count`
-     * key passed in `$vars`.
-     *
      * @param string $locale The locale in which the message is presented.
      * @param string|array $message The message to be translated
      * @param array $vars The list of values to interpolate in the message
@@ -41,23 +37,7 @@ class IcuFormatter implements FormatterInterface
      */
     public function format($locale, $message, array $vars)
     {
-        $isString = is_string($message);
-        if ($isString && isset($vars['_singular'])) {
-            $message = [$vars['_singular'], $message];
-            unset($vars['_singular']);
-            $isString = false;
-        }
-
-        if ($isString) {
-            return $this->_formatMessage($locale, $message, $vars);
-        }
-
-        if (!is_string($message)) {
-            $count = isset($vars['_count']) ? $vars['_count'] : 0;
-            unset($vars['_count'], $vars['_singular']);
-            $form = PluralRules::calculate($locale, $count);
-            $message = isset($message[$form]) ? $message[$form] : (string)end($message);
-        }
+        unset($vars['_singular'], $vars['_count']);
 
         return $this->_formatMessage($locale, $message, $vars);
     }

+ 1 - 29
src/I18n/Formatter/SprintfFormatter.php

@@ -28,10 +28,6 @@ class SprintfFormatter implements FormatterInterface
      * Returns a string with all passed variables interpolated into the original
      * message. Variables are interpolated using the sprintf format.
      *
-     * If an array is passed in `$message`, it will trigger the plural selection
-     * routine. Plural forms are selected depending on the locale and the `_count`
-     * key passed in `$vars`.
-     *
      * @param string $locale The locale in which the message is presented.
      * @param string|array $message The message to be translated
      * @param array $vars The list of values to interpolate in the message
@@ -39,31 +35,7 @@ class SprintfFormatter implements FormatterInterface
      */
     public function format($locale, $message, array $vars)
     {
-        if (is_string($message) && isset($vars['_singular'])) {
-            $message = [$vars['_singular'], $message];
-            unset($vars['_singular']);
-        }
-
-        if (is_string($message)) {
-            return vsprintf($message, $vars);
-        }
-
-        if (isset($vars['_context']) && isset($message['_context'])) {
-            $message = $message['_context'][$vars['_context']];
-            unset($vars['_context']);
-        }
-
-        // Assume first context when no context key was passed
-        if (isset($message['_context'])) {
-            $message = current($message['_context']);
-        }
-
-        if (!is_string($message)) {
-            $count = isset($vars['_count']) ? $vars['_count'] : 0;
-            unset($vars['_singular']);
-            $form = PluralRules::calculate($locale, $count);
-            $message = $message[$form];
-        }
+        unset($vars['_singular']);
 
         return vsprintf($message, $vars);
     }

+ 48 - 18
src/I18n/Translator.php

@@ -23,6 +23,7 @@ namespace Cake\I18n;
 use Aura\Intl\FormatterInterface;
 use Aura\Intl\Package;
 use Aura\Intl\TranslatorInterface;
+use Cake\I18n\PluralRules;
 
 /**
  * Provides missing message behavior for CakePHP internal message formats.
@@ -126,25 +127,8 @@ class Translator implements TranslatorInterface
 
         // Check for missing/invalid context
         if (isset($message['_context'])) {
-            $context = isset($tokensValues['_context']) ? $tokensValues['_context'] : null;
+            $message = $this->resolveContext($key, $message, $tokensValues);
             unset($tokensValues['_context']);
-
-            // No or missing context, fallback to the key/first message
-            if ($context === null) {
-                if (isset($message['_context'][''])) {
-                    $message = $message['_context'][''];
-                } else {
-                    $message = current($message['_context']);
-                }
-            } elseif (!isset($message['_context'][$context])) {
-                $message = $key;
-            } elseif (is_string($message['_context'][$context]) &&
-                strlen($message['_context'][$context]) === 0
-            ) {
-                $message = $key;
-            } else {
-                $message = $message['_context'][$context];
-            }
         }
 
         if (!$tokensValues) {
@@ -156,10 +140,56 @@ class Translator implements TranslatorInterface
             return $message;
         }
 
+        // Singular message, but plural call
+        if (is_string($message) && isset($tokensValues['_singular'])) {
+            $message = [$tokensValues['_singular'], $message];
+        }
+
+        // Resolve plural form.
+        if (is_array($message)) {
+            $count = isset($tokensValues['_count']) ? $tokensValues['_count'] : 0;
+            $form = PluralRules::calculate($this->locale, $count);
+            $message = isset($message[$form]) ? $message[$form] : (string)end($message);
+        }
+
+        if (strlen($message) === 0) {
+            $message = $key;
+        }
+
         return $this->formatter->format($this->locale, $message, $tokensValues);
     }
 
     /**
+     * Resolve a message's context structure.
+     *
+     * @param string $key The message key being handled.
+     * @param string|array $message The message content.
+     * @param array $vars The variables containing the `_context` key.
+     * @return string
+     */
+    protected function resolveContext($key, $message, array $vars)
+    {
+        $context = isset($vars['_context']) ? $vars['_context'] : null;
+
+        // No or missing context, fallback to the key/first message
+        if ($context === null) {
+            if (isset($message['_context'][''])) {
+                return $message['_context'][''];
+            }
+
+            return current($message['_context']);
+        }
+        if (!isset($message['_context'][$context])) {
+            return $key;
+        }
+        if ($message['_context'][$context] === '') {
+            return $key;
+        }
+
+        return $message['_context'][$context];
+    }
+
+    /**
      * An object of type Package
      *
      * @return \Aura\Intl\Package

+ 1 - 1
src/ORM/Query.php

@@ -197,7 +197,7 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
      * Hints this object to associate the correct types when casting conditions
      * for the database. This is done by extracting the field types from the schema
      * associated to the passed table object. This prevents the user from repeating
-     * himself when specifying conditions.
+     * themselves when specifying conditions.
      *
      * This method returns the same query object for chaining.
      *

+ 56 - 0
tests/TestCase/Console/ShellTest.php

@@ -310,6 +310,20 @@ class ShellTest extends TestCase
     }
 
     /**
+     * testErr method with array
+     *
+     * @return void
+     */
+    public function testErrArray()
+    {
+        $this->io->expects($this->once())
+            ->method('err')
+            ->with(['<error>Just</error>', '<error>a</error>', '<error>test</error>'], 1);
+
+        $this->Shell->err(['Just', 'a', 'test']);
+    }
+
+    /**
      * testInfo method
      *
      * @return void
@@ -324,6 +338,20 @@ class ShellTest extends TestCase
     }
 
     /**
+     * testInfo method with array
+     *
+     * @return void
+     */
+    public function testInfoArray()
+    {
+        $this->io->expects($this->once())
+            ->method('out')
+            ->with(['<info>Just</info>', '<info>a</info>', '<info>test</info>'], 1);
+
+        $this->Shell->info(['Just', 'a', 'test']);
+    }
+
+    /**
      * testWarn method
      *
      * @return void
@@ -338,6 +366,20 @@ class ShellTest extends TestCase
     }
 
     /**
+     * testWarn method with array
+     *
+     * @return void
+     */
+    public function testWarnArray()
+    {
+        $this->io->expects($this->once())
+            ->method('err')
+            ->with(['<warning>Just</warning>', '<warning>a</warning>', '<warning>test</warning>'], 1);
+
+        $this->Shell->warn(['Just', 'a', 'test']);
+    }
+
+    /**
      * testSuccess method
      *
      * @return void
@@ -352,6 +394,20 @@ class ShellTest extends TestCase
     }
 
     /**
+     * testSuccess method with array
+     *
+     * @return void
+     */
+    public function testSuccessArray()
+    {
+        $this->io->expects($this->once())
+            ->method('out')
+            ->with(['<success>Just</success>', '<success>a</success>', '<success>test</success>'], 1);
+
+        $this->Shell->success(['Just', 'a', 'test']);
+    }
+
+    /**
      * testNl
      *
      * @return void

+ 1 - 1
tests/TestCase/Http/Client/Adapter/StreamTest.php

@@ -420,7 +420,7 @@ class StreamTest extends TestCase
     /**
      * Test that an exception is raised when timed out.
      *
-     * @expectedException \Cake\Core\Exception\Exception
+     * @expectedException \Cake\Network\Exception\HttpException
      * @expectedExceptionMessage Connection timed out http://dummy/?sleep
      * @return void
      */

+ 4 - 2
tests/TestCase/Http/Client/ResponseTest.php

@@ -105,10 +105,12 @@ class ResponseTest extends TestCase
         $encoded = json_encode($data);
 
         $response = new Response([], $encoded);
-        $result = $response->body('json_decode');
-        $this->assertEquals($data['property'], $result->property);
+
+        $this->assertEquals($encoded, $response->getBody()->getContents());
         $this->assertEquals($encoded, $response->body());
 
+        $result = $response->body('json_decode');
+        $this->assertEquals($data['property'], $result->property);
         $this->assertEquals($encoded, $response->body);
         $this->assertTrue(isset($response->body));
     }

+ 0 - 36
tests/TestCase/I18n/Formatter/IcuFormatterTest.php

@@ -51,26 +51,6 @@ class IcuFormatterTest extends TestCase
     }
 
     /**
-     * Tests that plural forms can be selected using the PO file format plural forms
-     *
-     * @return void
-     */
-    public function testFormatPlural()
-    {
-        $formatter = new IcuFormatter();
-        $messages = [
-            '{0} is 0',
-            '{0} is 1',
-            '{0} is 2',
-            '{0} is 3',
-            '{0} > 11'
-        ];
-        $this->assertEquals('1 is 1', $formatter->format('ar', $messages, ['_count' => 1, 1]));
-        $this->assertEquals('2 is 2', $formatter->format('ar', $messages, ['_count' => 2, 2]));
-        $this->assertEquals('20 > 11', $formatter->format('ar', $messages, ['_count' => 20, 20]));
-    }
-
-    /**
      * Tests that plurals can instead be selected using ICU's native selector
      *
      * @return void
@@ -130,20 +110,4 @@ class IcuFormatterTest extends TestCase
         $formatter = new IcuFormatter();
         $formatter->format('en_US', '{crazy format', ['some', 'vars']);
     }
-
-    /**
-     * Tests that it is possible to provide a singular fallback when passing a string message.
-     * This is useful for getting quick feedback on the code during development instead of
-     * having to provide all plural forms even for the default language
-     *
-     * @return void
-     */
-    public function testSingularFallback()
-    {
-        $formatter = new IcuFormatter();
-        $singular = 'one thing';
-        $plural = 'many things';
-        $this->assertEquals($singular, $formatter->format('en_US', $plural, ['_count' => 1, '_singular' => $singular]));
-        $this->assertEquals($plural, $formatter->format('en_US', $plural, ['_count' => 2, '_singular' => $singular]));
-    }
 }

+ 0 - 79
tests/TestCase/I18n/Formatter/SprintfFormatterTest.php

@@ -34,83 +34,4 @@ class SprintfFormatterTest extends TestCase
         $this->assertEquals('Hello José', $formatter->format('en_US', 'Hello %s', ['José']));
         $this->assertEquals('1 Orange', $formatter->format('en_US', '%d %s', [1, 'Orange']));
     }
-
-    /**
-     * Tests that plural forms are selected for the passed locale
-     *
-     * @return void
-     */
-    public function testFormatPlural()
-    {
-        $formatter = new SprintfFormatter();
-        $messages = ['%d is 0', '%d is 1', '%d is 2', '%d is 3', '%d > 11'];
-        $this->assertEquals('1 is 1', $formatter->format('ar', $messages, ['_count' => 1]));
-        $this->assertEquals('2 is 2', $formatter->format('ar', $messages, ['_count' => 2]));
-        $this->assertEquals('20 > 11', $formatter->format('ar', $messages, ['_count' => 20]));
-    }
-
-    /**
-     * Tests that strings stored inside context namespaces can also be formatted
-     *
-     * @return void
-     */
-    public function testFormatWithContext()
-    {
-        $messages = [
-            'simple' => [
-                '_context' => [
-                    'context a' => 'Text "a" %s',
-                    'context b' => 'Text "b" %s'
-                ]
-            ],
-            'complex' => [
-                '_context' => [
-                    'context b' => [
-                        0 => 'Only one',
-                        1 => 'there are %d'
-                    ]
-                ]
-            ]
-        ];
-
-        $formatter = new SprintfFormatter();
-        $this->assertEquals(
-            'Text "a" is good',
-            $formatter->format('en', $messages['simple'], ['_context' => 'context a', 'is good'])
-        );
-        $this->assertEquals(
-            'Text "b" is good',
-            $formatter->format('en', $messages['simple'], ['_context' => 'context b', 'is good'])
-        );
-        $this->assertEquals(
-            'Text "a" is good',
-            $formatter->format('en', $messages['simple'], ['is good'])
-        );
-
-        $this->assertEquals(
-            'Only one',
-            $formatter->format('en', $messages['complex'], ['_context' => 'context b', '_count' => 1])
-        );
-
-        $this->assertEquals(
-            'there are 2',
-            $formatter->format('en', $messages['complex'], ['_context' => 'context b', '_count' => 2])
-        );
-    }
-
-    /**
-     * Tests that it is possible to provide a singular fallback when passing a string message.
-     * This is useful for getting quick feedback on the code during development instead of
-     * having to provide all plural forms even for the default language
-     *
-     * @return void
-     */
-    public function testSingularFallback()
-    {
-        $formatter = new SprintfFormatter();
-        $singular = 'one thing';
-        $plural = 'many things';
-        $this->assertEquals($singular, $formatter->format('en_US', $plural, ['_count' => 1, '_singular' => $singular]));
-        $this->assertEquals($plural, $formatter->format('en_US', $plural, ['_count' => 2, '_singular' => $singular]));
-    }
 }

+ 21 - 1
tests/TestCase/I18n/I18nTest.php

@@ -281,7 +281,7 @@ class I18nTest extends TestCase
     }
 
     /**
-     * Tests the __() function on a plural key
+     * Tests the __() function on a plural key works
      *
      * @return void
      */
@@ -313,6 +313,18 @@ class I18nTest extends TestCase
     }
 
     /**
+     * Tests the __n() function on singular keys
+     *
+     * @return void
+     */
+    public function testBasicTranslatePluralFunctionSingularMessage()
+    {
+        I18n::defaultFormatter('sprintf');
+        $result = __n('No translation needed', 'not used', 1);
+        $this->assertEquals('No translation needed', $result);
+    }
+
+    /**
      * Tests the __d() function
      *
      * @return void
@@ -325,11 +337,13 @@ class I18nTest extends TestCase
                 'Cow' => 'Le moo',
                 'The {0} is tasty' => 'The {0} is delicious',
                 'Average price {0}' => 'Price Average {0}',
+                'Unknown' => '',
             ]);
 
             return $package;
         }, 'en_US');
         $this->assertEquals('Le moo', __d('custom', 'Cow'));
+        $this->assertEquals('Unknown', __d('custom', 'Unknown'));
 
         $result = __d('custom', 'The {0} is tasty', ['fruit']);
         $this->assertEquals('The fruit is delicious', $result);
@@ -355,6 +369,10 @@ class I18nTest extends TestCase
                 'Cows' => [
                     'Le Moo',
                     'Les Moos'
+                ],
+                '{0} years' => [
+                    '',
+                    ''
                 ]
             ]);
 
@@ -362,6 +380,8 @@ class I18nTest extends TestCase
         }, 'en_US');
         $this->assertEquals('Le Moo', __dn('custom', 'Cow', 'Cows', 1));
         $this->assertEquals('Les Moos', __dn('custom', 'Cow', 'Cows', 2));
+        $this->assertEquals('{0} years', __dn('custom', '{0} year', '{0} years', 1));
+        $this->assertEquals('{0} years', __dn('custom', '{0} year', '{0} years', 2));
     }
 
     /**

+ 53 - 2
tests/TestCase/I18n/Parser/PoFileParserTest.php

@@ -15,6 +15,7 @@
 namespace Cake\Test\TestCase\I18n\Parser;
 
 use Aura\Intl\Package;
+use Cake\Cache\Cache;
 use Cake\I18n\I18n;
 use Cake\I18n\Parser\PoFileParser;
 use Cake\TestSuite\TestCase;
@@ -24,6 +25,31 @@ use Cake\TestSuite\TestCase;
  */
 class PoFileParserTest extends TestCase
 {
+    protected $locale;
+
+    /**
+     * Set Up
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $this->locale = I18n::locale();
+    }
+
+    /**
+     * Tear down method
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        parent::tearDown();
+        I18n::clear();
+        I18n::locale($this->locale);
+        Cache::clear(false, '_cake_core_');
+    }
 
     /**
      * Tests parsing a file with plurals and message context
@@ -119,7 +145,7 @@ class PoFileParserTest extends TestCase
         $file = APP . 'Locale' . DS . 'en' . DS . 'context.po';
         $messages = $parser->parse($file);
 
-        I18n::translator('default', 'de_DE', function () use ($messages) {
+        I18n::translator('default', 'en_CA', function () use ($messages) {
             $package = new Package('default');
             $package->setMessages($messages);
 
@@ -131,10 +157,35 @@ class PoFileParserTest extends TestCase
         $this->assertSame('En resolved - context', $messages['Resolved']['_context']['Pay status']);
 
         // Confirm actual behavior
-        I18n::locale('de_DE');
+        I18n::locale('en_CA');
         $this->assertSame('En cours', __('Pending'));
         $this->assertSame('En cours - context', __x('Pay status', 'Pending'));
         $this->assertSame('En resolved', __('Resolved'));
         $this->assertSame('En resolved - context', __x('Pay status', 'Resolved'));
     }
+
+    /**
+     * Test parsing context based messages
+     *
+     * @return void
+     */
+    public function testParseContextMessages()
+    {
+        $parser = new PoFileParser();
+        $file = APP . 'Locale' . DS . 'en' . DS . 'context.po';
+        $messages = $parser->parse($file);
+
+        I18n::translator('default', 'en_US', function () use ($messages) {
+            $package = new Package('default');
+            $package->setMessages($messages);
+
+            return $package;
+        });
+
+        // Check translated messages
+        I18n::locale('en_US');
+        $this->assertSame('Titel mit Kontext', __x('context', 'title'));
+        $this->assertSame('Titel mit anderem Kontext', __x('another_context', 'title'));
+        $this->assertSame('Titel ohne Kontext', __('title'));
+    }
 }

+ 14 - 0
tests/test_app/TestApp/Locale/en/context.po

@@ -13,3 +13,17 @@ msgstr "En resolved - context"
 
 msgid "Resolved"
 msgstr "En resolved"
+
+#: No context
+msgid "title"
+msgstr "Titel ohne Kontext"
+
+#: Has context
+msgctxt "context"
+msgid "title"
+msgstr "Titel mit Kontext"
+
+#: Another context
+msgctxt "another_context"
+msgid "title"
+msgstr "Titel mit anderem Kontext"