Skip to content

Commit

Permalink
Fix context/plural related issues in i18n
Browse files Browse the repository at this point in the history
Because we were using the stock translator from Aura, we had painful
issues when:

* A plural message was used with `__()` an array would be returned which
  is never right.
* When an unknown context is used the `__x()` method fails.

Refs cakephp#8833
Refs cakephp#9985
  • Loading branch information
markstory committed Jan 10, 2017
1 parent 475b191 commit 0c4ae66
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 61 deletions.
10 changes: 0 additions & 10 deletions src/I18n/Formatter/IcuFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,6 @@ public function format($locale, $message, array $vars)
return $this->_formatMessage($locale, $message, $vars);
}

if (isset($vars['_context'], $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['_count'], $vars['_singular']);
Expand Down
2 changes: 1 addition & 1 deletion src/I18n/I18n.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

use Aura\Intl\FormatterLocator;
use Aura\Intl\PackageLocator;
use Aura\Intl\TranslatorFactory;
use Cake\Cache\Cache;
use Cake\I18n\Formatter\IcuFormatter;
use Cake\I18n\Formatter\SprintfFormatter;
use Cake\I18n\TranslatorFactory;
use Locale;

/**
Expand Down
151 changes: 151 additions & 0 deletions src/I18n/Translator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
*
* This file contains sections from the Aura Project
* @license https://github.com/auraphp/Aura.Intl/blob/3.x/LICENSE
*
* The Aura Project for PHP.
*
* @package Aura.Intl
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace Cake\I18n;

use Aura\Intl\TranslatorInterface;
use Aura\Intl\FormatterInterface;

/**
* Provides missing message behavior for CakePHP internal message formats.
*
* @internal
*/
class Translator implements TranslatorInterface
{
/**
* A fallback translator.
*
* @var \Aura\Intl\TranslatorInterface
*/
protected $fallback;

/**
* The formatter to use when translating messages.
*
* @var \Aura\Intl\FormatterInterface
*/
protected $formatter;

/**
* The locale being used for translations.
*
* @var string
*/
protected $locale;

/**
* The message keys and translations.
*
* @var array
*/
protected $messages = [];

/**
* Constructor
*
* @param string $locale The locale being used.
* @param array $messages The message keys and translations.
* @param \Aura\Intl\FormatterInterface $formatter A message formatter.
* @param \Aura\Intl\TranslatorInterface $fallback A fallback translator.
*/
public function __construct(
$locale,
array $messages,
FormatterInterface $formatter,
TranslatorInterface $fallback = null
) {
$this->locale = $locale;
$this->messages = $messages;
$this->formatter = $formatter;
$this->fallback = $fallback;
}

/**
* Gets the message translation by its key.
*
* @param string $key The message key.
* @return mixed The message translation string, or false if not found.
*/
protected function getMessage($key)
{
if (isset($this->messages[$key])) {
return $this->messages[$key];
}

if ($this->fallback) {
// get the message from the fallback translator
$message = $this->fallback->getMessage($key);
// speed optimization: retain locally
$this->messages[$key] = $message;
// done!
return $message;
}

// no local message, no fallback
return false;
}

/**
* Translates the message formatting any placeholders
*
*
* @param string $key The message key.
* @param array $tokensValues Token values to interpolate into the
* message.
* @return string The translated message with tokens replaced.
*/
public function translate($key, array $tokensValues = [])
{
$message = $this->getMessage($key);

if (!$message) {
// Fallback to the message key
$message = $key;
}

// Check for missing/invalid context
if (isset($message['_context'])) {
$context = isset($tokensValues['_context']) ? $tokensValues['_context'] : null;
unset($tokensValues['_context']);

// No or missing context, fallback to the key/first message
if ($context === null) {
$message = current($message['_context']);
} elseif(!isset($message['_context'][$context])) {
$message = $key;
} elseif (!isset($message['_context'][$context])) {
$message = $key;
} else {
$message = $message['_context'][$context];
}
}

if (!$tokensValues) {
// Fallback for plurals that were using the singular key
if (is_array($message)) {
return array_values($message + [''])[0];
}
return $message;
}

return $this->formatter->format($this->locale, $message, $tokensValues);
}
}
35 changes: 35 additions & 0 deletions src/I18n/TranslatorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.3.12
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\I18n;

use Aura\Intl\TranslatorFactory as BaseTranslatorFactory;

use Aura\Intl\TranslatorInterface;
use Aura\Intl\FormatterInterface;

/**
* Factory to create translators
*
* @internal
*/
class TranslatorFactory extends BaseTranslatorFactory
{
/**
* The class to use for new instances.
*
* @var string
*/
protected $class = 'Cake\I18n\Translator';
}
49 changes: 0 additions & 49 deletions tests/TestCase/I18n/Formatter/IcuFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,55 +124,6 @@ public function testBadMessageFormat()
$formatter->format('en_US', '{crazy format', ['some', 'vars']);
}

/**
* Tests that strings stored inside context namespaces can also be formatted
*
* @return void
*/
public function testFormatWithContext()
{
$messages = [
'simple' => [
'_context' => [
'context a' => 'Text "a" {0}',
'context b' => 'Text "b" {0}'
]
],
'complex' => [
'_context' => [
'context b' => [
0 => 'Only one',
1 => 'there are {0}'
]
]
]
];

$formatter = new IcuFormatter();
$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, 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
Expand Down
37 changes: 36 additions & 1 deletion tests/TestCase/I18n/I18nTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function tearDown()
public function testDefaultTranslator()
{
$translator = I18n::translator();
$this->assertInstanceOf('Aura\Intl\Translator', $translator);
$this->assertInstanceOf('Aura\Intl\TranslatorInterface', $translator);
$this->assertEquals('%d is 1 (po translated)', $translator->translate('%d = 1'));
}

Expand Down Expand Up @@ -244,6 +244,17 @@ public function testBasicTranslateFunction()
$this->assertEquals('1 is 1 (po translated)', __('%d = 1', 1));
}

/**
* Tests the __() function on a plural key
*
* @return void
*/
public function testBasicTranslateFunctionPluralData()
{
I18n::defaultFormatter('sprintf');
$this->assertEquals('%d is 1 (po translated)', __('%d = 0 or > 1'));
}

/**
* Tests the __n() function
*
Expand Down Expand Up @@ -386,6 +397,30 @@ public function testBasicContextFunctionNoString()
$this->assertEquals('', __x('character', 'letter'));
}

/**
* Tests the __x() function with an invalid context
*
* @return void
*/
public function testBasicContextFunctionInvalidContext()
{
I18n::translator('default', 'en_US', function () {
$package = new Package('default');
$package->setMessages([
'letter' => [
'_context' => [
'noun' => 'a paper letter',
]
]
]);

return $package;
});

$this->assertEquals('letter', __x('garbage', 'letter'));
$this->assertEquals('a paper letter', __('letter'));
}

/**
* Tests the __xn() function
*
Expand Down

0 comments on commit 0c4ae66

Please sign in to comment.