Skip to content

Commit

Permalink
[Translate] add an intl MessageFormatter interpolator (phalcon#892)
Browse files Browse the repository at this point in the history
  • Loading branch information
challet authored and ruudboon committed Aug 30, 2019
1 parent bbb6b80 commit f446b30
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 133 deletions.
32 changes: 0 additions & 32 deletions Library/Phalcon/Translate/Adapter/Base.php

This file was deleted.

31 changes: 4 additions & 27 deletions Library/Phalcon/Translate/Adapter/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
use Phalcon\Translate\AdapterInterface;
use Phalcon\Translate\Exception;

class Database extends Base implements AdapterInterface, \ArrayAccess
class Database extends Adapter implements AdapterInterface, \ArrayAccess
{
/**
* @var array
Expand All @@ -44,13 +44,6 @@ class Database extends Base implements AdapterInterface, \ArrayAccess
*/
protected $stmtSelect;

/**
* Use ICU MessageFormatter to parse message
*
* @var boolean
*/
protected $useIcuMessageFormatter = false;

/**
* Class constructor.
*
Expand All @@ -71,14 +64,6 @@ public function __construct(array $options)
throw new Exception("Parameter 'language' is required");
}

if (isset($options['useIcuMessageFormatter'])) {
if (!class_exists('\MessageFormatter')) {
throw new Exception('"MessageFormatter" class is required');
}

$this->useIcuMessageFormatter = (boolean) $options['useIcuMessageFormatter'];
}

$this->stmtSelect = sprintf(
'SELECT value FROM %s WHERE language = :language AND key_name = :key_name',
$options['table']
Expand All @@ -90,6 +75,8 @@ public function __construct(array $options)
);

$this->options = $options;

parent::__construct($options);
}

/**
Expand All @@ -109,17 +96,7 @@ public function query($translateKey, $placeholders = null)
);
$value = empty($translation['value']) ? $translateKey : $translation['value'];

if (is_array($placeholders) && !empty($placeholders)) {
if (true === $this->useIcuMessageFormatter) {
$value = \MessageFormatter::formatMessage($options['language'], $value, $placeholders);
} else {
foreach ($placeholders as $placeHolderKey => $placeHolderValue) {
$value = str_replace('%' . $placeHolderKey . '%', $placeHolderValue, $value);
}
}
}

return $value;
return $this->replacePlaceholders($value, $placeholders);
}

/**
Expand Down
49 changes: 5 additions & 44 deletions Library/Phalcon/Translate/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use Phalcon\Translate\Exception;
use Phalcon\Mvc\CollectionInterface;
use Phalcon\Translate\Adapter;
use Phalcon\Translate\AdapterInterface;

/**
Expand All @@ -33,11 +34,10 @@
*
* @package Phalcon\Translate\Adapter
*/
class Mongo extends Base implements AdapterInterface, \ArrayAccess
class Mongo extends Adapter implements AdapterInterface, \ArrayAccess
{
protected $language;
protected $collection;
protected $formatter;

/**
* Mongo constructor.
Expand All @@ -59,10 +59,8 @@ public function __construct($options)
}

$this->setLanguage($options['language']);

if (isset($options['formatter'])) {
$this->setFormatter($options['formatter']);
}

parent::__construct($options);
}

/**
Expand All @@ -86,16 +84,6 @@ protected function setLanguage($language)
$this->language = $language;
}

/**
* Sets the formatter to use if necessary.
*
* @param \MessageFormatter $formatter Message formatter.
*/
protected function setFormatter(\MessageFormatter $formatter)
{
$this->formatter = $formatter;
}

/**
* Gets the translations set.
*
Expand Down Expand Up @@ -132,34 +120,7 @@ public function query($translateKey, $placeholders = null)
$translation = $translations->{$this->language};
}

if (!empty($placeholders)) {
return $this->format($translation, $placeholders);
}

return $translation;
}

/**
* Formats a translation.
*
* @param string $translation Translated text.
* @param array $placeholders Placeholders to apply.
*
* @return string
*/
protected function format($translation, $placeholders = [])
{
if ($this->formatter) {
$formatter = $this->formatter;

return $formatter::formatMessage($this->language, $translation, $placeholders);
}

foreach ($placeholders as $key => $value) {
$translation = str_replace("%$key%", $value, $translation);
}

return $translation;
return $this->replacePlaceholders($value, $placeholders);
}

/**
Expand Down
17 changes: 1 addition & 16 deletions Library/Phalcon/Translate/Adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ class IndexController extends \Phalcon\Mvc\Controller
'db' => $this->di->get('db'), // Here we're getting the database from DI
'table' => 'translations', // The table that is storing the translations
'language' => $this->request->getBestLanguage(), // Now we're getting the best language for the user
'useIcuMessageFormatter' => true, // Optional, if need formatting message using ICU MessageFormatter
]);
}

Expand Down Expand Up @@ -98,14 +97,6 @@ Or, if you wish you can use [Volt][2]:
<h1>{{ expression._("IndexPage_Hello_World") }}</h1>
```

ICU MessageFormatter Example
```php
// Example plural message with key 'cats'
// Peter has {nbCats, plural, =0{no cat} =1{a cat} other{# cats}}

$this->_getTranslation()->_('cats', ['nbCats' => rand(0, 10)]);
```

## Mongo

Implements a Mongo adapter for translations.
Expand All @@ -117,15 +108,9 @@ use MessageFormatter;
use Phalcon\Translate\Adapter\Mongo;
use My\Application\Collections\Translate;

$fmt = new MessageFormatter(
"en_US",
"{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree"
);

$translate = new Mongo([
'collection' => Translate::class,
'language' => 'en',
'formatter' => $fmt,
'language' => 'en'
]);

echo $translate->t('application.title');
Expand Down
9 changes: 7 additions & 2 deletions Library/Phalcon/Translate/Adapter/Redis.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace Phalcon\Translate\Adapter;

use Phalcon\Translate\Adapter;
use Phalcon\Translate\AdapterInterface;
use Phalcon\Translate\Exception;

Expand All @@ -9,7 +10,7 @@
*
* @package Phalcon\Translate\Adapter
*/
class Redis extends Base implements AdapterInterface
class Redis extends Adapter implements AdapterInterface
{
/**
* Redis object.
Expand Down Expand Up @@ -61,6 +62,8 @@ public function __construct(array $options)
if (isset($options['levels'])) {
$this->levels = $options['levels'];
}

parent::__construct($options);
}

/**
Expand Down Expand Up @@ -93,9 +96,11 @@ public function query($translateKey, $placeholders = null)

$this->loadValueByKey($key);

return isset($this->cache[$key]) && isset($this->cache[$key][$index])
$value = isset($this->cache[$key]) && isset($this->cache[$key][$index])
? $this->cache[$key][$index]
: $translateKey;

return $this->replacePlaceholders($value, $placeholders);
}

/**
Expand Down
16 changes: 4 additions & 12 deletions Library/Phalcon/Translate/Adapter/ResourceBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/**
* ResourceBundle adapter
*/
class ResourceBundle extends Base implements AdapterInterface
class ResourceBundle extends Adapter implements AdapterInterface
{
/**
* @var \ResourceBundle
Expand Down Expand Up @@ -42,10 +42,6 @@ public function __construct($options)
throw new Exception('"ResourceBundle" class is required');
}

if (!class_exists('\MessageFormatter')) {
throw new Exception('"MessageFormatter" class is required');
}

if (!isset($options['bundle'])) {
throw new Exception('"bundle" option is required');
}
Expand All @@ -60,6 +56,8 @@ public function __construct($options)

$this->options = $options;
$this->bundle = new \ResourceBundle($this->options['locale'], $this->options['bundle'], $this->fallback);

parent::__construct($options);
}

/**
Expand Down Expand Up @@ -88,13 +86,7 @@ public function query($index, $placeholders = null)
return $index;
}

$formatter = new \MessageFormatter($this->options['locale'], $this->get($index, $this->bundle));

if (null !== $formatter) {
return $formatter->format((array) $placeholders);
} else {
return $index;
}
return $this->replacePlaceholders($this->get($index, $this->bundle), $placeholders);
}

/**
Expand Down
52 changes: 52 additions & 0 deletions Library/Phalcon/Translate/Interpolator/Intl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Phalcon\Translate\Interpolator;

use Phalcon\Translate\InterpolatorInterface;
use Phalcon\Translate\Exception;
use MessageFormatter;
use IntlException;

class Intl implements InterpolatorInterface
{
private $locale;

public function __construct($locale)
{
$this->locale = $locale;
}

/**
* Replaces placeholders by the values passed
* Use the MessageFormatter class,
* See http://php.net/manual/en/class.messageformatter.php
*/
public function replacePlaceholders($translation, $placeholders = null)
{
if (is_array($placeholders) && count($placeholders)) {
try {
// TODO (?) : keep an internal cache of the MessageFormatter objects (key = locale.translation)
$fmt = new MessageFormatter($this->locale, $translation);
} catch (IntlException $e) {
$fmt = null;
} finally {
// for php 7.x the original exception message is "Constructor failed"
// for php 5.6 the constructor returns null, see this wont fix bug https://bugs.php.net/bug.php?id=58631
// make it a bit more understandable
if (is_null($fmt)) {
throw new Exception(
"Unable to instantiate a MessageFormatter. Check locale and string syntax.",
0,
isset($e) ? $e : null
);
}
}

$translation = $fmt->format($placeholders);
if ($translation === false) {
throw new Exception($fmt->getErrorMessage(), $fmt->getErrorCode());
}
}
return $translation;
}
}
36 changes: 36 additions & 0 deletions Library/Phalcon/Translate/Interpolator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Phalcon\Translate\Interpolator

Usage examples of the interpolators available here:

## Intl

It needs the extension [intl](php.net/manual/book.intl.php) to be installed in PHP, and it uses [MessageFormatter](http://php.net/manual/en/class.messageformatter.php) objects in an interpolator interface.
More about the syntax convention can be read on this [formating guide](https://www.sitepoint.com/localization-demystified-understanding-php-intl/) and on the [ICU documentation](http://userguide.icu-project.org/formatparse/messages).

```php
<?php
use Phalcon\Translate\Adapter\NativeArray;
use Phalcon\Translate\Interpolator\Intl;

$translate = new NativeArray([
'interpolator' => new Intl('en_US'), // this interpolator must be locale aware
'content' => ['hi-name' => 'Hello {name}, it\'s {time, number, integer} o\'clock']
]);

$name = 'Henry';
$translate->_('hi-name', ['name' => $name, 'time' => 8]); // Hello Henry, it's 8 o'clock
```

```php
<?php
use Phalcon\Translate\Adapter\NativeArray;
use Phalcon\Translate\Interpolator\Intl;

$translate = new NativeArray([
'interpolator' => new Intl('fr_FR'), // this interpolator must be locale aware
'content' => ['apples' => "{count, plural, =0{Je n'ai aucune pomme} =1{J'ai une pomme} other{J'ai # pommes}}."]
]);

// thousands separator is " " (blank space) for fr_FR
echo $translate->_('apples', ['count' => 1000]); // J'ai 1 000 pommes
```
Loading

0 comments on commit f446b30

Please sign in to comment.