Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch '0.3-g11n' into 0.3

  • Loading branch information...
commit 3d06acf1612b99cc4f42bdd892c413e9425d88d2 2 parents b7b33e2 + e1ae0a5
@gwoo gwoo authored
View
16 app/config/bootstrap.php
@@ -131,16 +131,24 @@
// use lithium\g11n\Catalog;
//
// Catalog::config(array(
-// 'runtime' => array('adapter' => 'Memory'),
-// 'app' => array('adapter' => 'Gettext', 'path' => LITHIUM_APP_PATH . '/resources/po'),
-// 'lithium' => array('adapter' => 'Gettext', 'path' => LITHIUM_LIBRARY_PATH . '/lithium/resources/po')
+// 'runtime' => array(
+// 'adapter' => 'Memory'
+// ),
+// 'app' => array(
+// 'adapter' => 'Gettext',
+// 'path' => LITHIUM_APP_PATH . '/resources/g11n'
+// ),
+// 'lithium' => array(
+// 'adapter' => 'Gettext',
+// 'path' => LITHIUM_LIBRARY_PATH . '/lithium/g11n/resources'
+// )
// ));
/**
* Globalization runtime data. You can add globalized data during runtime utilizing a
* configuration set up to use the _memory_ adapter.
*/
-// $data = array('en' => function($n) { return $n != 1 ? 1 : 0; });
+// $data = array('root' => function($n) { return $n != 1 ? 1 : 0; });
// Catalog::write('message.plural', $data, array('name' => 'runtime'));
/**
View
35 app/config/switchboard.php
@@ -19,11 +19,13 @@
use \lithium\http\Router;
use \lithium\core\Environment;
use \lithium\action\Dispatcher;
+use \lithium\g11n\Message;
+use \lithium\util\String;
/**
* Loads application routes before the request is dispatched. Change this to `include_once` if
* more than one request cycle is executed per HTTP request.
- *
+ *
* @see lithium\http\Router
*/
Dispatcher::applyFilter('run', function($self, $params, $chain) {
@@ -34,7 +36,7 @@
/**
* Intercepts the `Dispatcher` as it finds a controller object, and passes the `'request'` parameter
* to the `Environment` class to detect which environment the application is running in.
- *
+ *
* @see lithium\action\Request
* @see lithium\core\Environment
*/
@@ -43,4 +45,33 @@
return $chain->next($self, $params, $chain);
});
+/**
+ * Implements logic for handling cases where `Message::translate()` returns without a result.
+ * The message specified for the `'default'` option will be used as a fall back. By
+ * default the value for the options is the message passed to the method.
+ */
+Message::applyFilter('translate', function($self, $params, $chain) {
+ $params['options'] += array('default' => $params['id']);
+ return $chain->next($self, $params, $chain) ?: $params['options']['default'];
+});
+
+/**
+ * Placeholders in translated messages. Adds support for `String::insert()`-style placeholders
+ * to translated messages. Placeholders may be used within the message and replacements provided
+ * directly within the `options` argument.
+ *
+ * Usage:
+ * {{{
+ * Message::translate('Your {:color} paintings are looking just great.', array(
+ * 'color' => 'silver',
+ * 'locale' => 'fr'
+ * ));
+ * }}}
+ *
+ * @see lithium\util\String::insert()
+ */
+Message::applyFilter('translate', function($self, $params, $chain) {
+ return String::insert($chain->next($self, $params, $chain), $params['options']);
+});
+
?>
View
2  libraries/lithium/console/command/G11n.php
@@ -17,7 +17,7 @@
class G11n extends \lithium\console\Command {
/**
- * The main method of the commad.
+ * The main method of the command.
*
* @return void
*/
View
145 libraries/lithium/console/command/g11n/Extract.php
@@ -11,55 +11,52 @@
use \Exception;
use \DateTime;
use \lithium\g11n\Catalog;
-use \lithium\util\String;
/**
* The `Extract` class is a command for extracting messages from files.
*/
class Extract extends \lithium\console\Command {
+ public $source;
+
+ public $destination;
+
+ public $scope;
+
+ public function _init() {
+ parent::_init();
+ $this->source = $this->source ?: LITHIUM_APP_PATH;
+ $this->destination = $this->destination ?: LITHIUM_APP_PATH . '/resources/g11n';
+ }
+
/**
- * The main method of the commad.
+ * The main method of the command.
*
* @return void
*/
public function run() {
- $sourcePath = LITHIUM_APP_PATH;
- $destinationPath = LITHIUM_APP_PATH . '/resources/po/';
+ $this->header('Message Extraction');
- $this->out('Extracting messages from source code.');
- $this->hr();
- $timeStart = microtime(true);
-
- $data = $this->_extract($sourcePath);
+ if (!$data = $this->_extract()) {
+ $this->err('Yielded no items.');
+ return 1;
+ }
+ $count = count($data['root']);
+ $this->out("Yielded {$count} items.");
$this->nl();
- $this->out(String::insert('Yielded {:countItems} items taking {:duration} seconds.', array(
- 'countItems' => count($data['root']),
- 'duration' => round(microtime(true) - $timeStart, 4)
- )));
- $this->nl();
- $this->out('Additional data.');
- $this->hr();
+ $this->header('Message Template Creation');
$meta = $this->_meta();
-
$this->nl();
- $this->out('Messages template.');
- $this->hr();
- $message = 'Would you like to save the template now? ';
- $message .= '(An existing template will be overwritten)';
-
- if ($this->in($message, array('choices' => array('y', 'n'), 'default' => 'n')) != 'y') {
- $this->stop(1, 'Aborting upon user request.');
+ if (!$this->_writeTemplate($data, $meta)) {
+ $this->err('Failed to write template.');
+ return 1;
}
$this->nl();
- $this->_writeTemplate($data, $meta);
-
- $this->nl();
- $this->out('Done.');
+ return 0;
}
/**
@@ -68,11 +65,41 @@ public function run() {
* @param array $files Absolute paths to files
* @return array
*/
- protected function _extract($path) {
- Catalog::config(array(
- 'extract' => array('adapter' => 'Code', 'path' => $path)
+ protected function _extract() {
+ $message[] = 'A `Catalog` class configuration with an adapter that is capable of';
+ $message[] = 'handling read requests for the `message.template` category is needed';
+ $message[] = 'in order to proceed.';
+ $this->out($message);
+ $this->nl();
+
+ $configs = (array)Catalog::config()->to('array');
+
+ $this->out('Available `Catalog` Configurations:');
+ foreach ($configs as $name => $config) {
+ $this->out(" - {$name}");
+ }
+ $this->nl();
+
+ $name = $this->in('Please choose a configuration or hit [enter] to add one:', array(
+ 'choices' => array_keys($configs)
));
- return Catalog::read('message.template', 'root', array('name' => 'extract'));
+
+ if (!$name) {
+ $adapter = $this->in('Adapter:', array(
+ 'default' => 'Gettext'
+ ));
+ $path = $this->in('Path:', array(
+ 'default' => $this->destination
+ ));
+ $scope = $this->in('Scope:', array(
+ 'default' => $this->scope
+ ));
+ $name = 'runtime' . uniqid();
+ $configs[$name] = compact('adapter', 'path', 'scope');
+ }
+ Catalog::config($configs);
+ $scope = $configs[$name]['scope'];
+ return Catalog::read('message.template', 'root', compact('name', 'scope'));
}
/**
@@ -81,6 +108,11 @@ protected function _extract($path) {
* @return array
*/
protected function _meta() {
+ $message[] = 'Please provide some data which is used when creating the';
+ $message[] = 'template.';
+ $this->out($message);
+ $this->nl();
+
$now = new DateTime();
return array(
'package' => $this->in('Package name:', array('default' => 'app')),
@@ -100,13 +132,52 @@ protected function _meta() {
* @return void
*/
protected function _writeTemplate($data, $meta) {
- $configs = Catalog::config()->to('array');
- $name = $this->in('Please choose a config:', array(
- 'choices' => array_keys($configs),
- 'default' => 'extract'
+ $message[] = 'In order to proceed you need to choose a `Catalog` configuration';
+ $message[] = 'which is used for writing the template. The adapter for the configuration';
+ $message[] = 'should be capable of handling write requests for the `message.template`';
+ $message[] = 'category.';
+ $this->out($message);
+ $this->nl();
+
+ $configs = (array)Catalog::config()->to('array');
+
+ $this->out('Available `Catalog` Configurations:');
+ foreach ($configs as $name => $config) {
+ $this->out(" - {$name}");
+ }
+ $this->nl();
+
+ $name = $this->in('Please choose a configuration or hit [enter] to add one:', array(
+ 'choices' => array_keys($configs)
));
- Catalog::write('message.template', 'root', compact('name'));
+ if (!$name) {
+ $adapter = $this->in('Adapter:', array(
+ 'default' => 'Gettext'
+ ));
+ $path = $this->in('Path:', array(
+ 'default' => $this->destination
+ ));
+ $scope = $this->in('Scope:', array(
+ 'default' => $this->scope
+ ));
+ $name = 'runtime' . uniqid();
+ $configs[$name] = compact('adapter', 'path', 'scope');
+ Catalog::config($configs);
+ }
+ $scope = $configs[$name]['scope'] ?: $this->in('Scope:', array('default' => $this->scope));
+
+ $message = array();
+ $message[] = 'The template is now ready to be saved.';
+ $message[] = 'Please note that an existing template will be overwritten.';
+ $this->out($message);
+ $this->nl();
+
+ if ($this->in('Save?', array('choices' => array('y', 'n'), 'default' => 'n')) != 'y') {
+ $this->out('Aborting upon user request.');
+ $this->stop(1);
+ }
+ return Catalog::write('message.template', $data, compact('name', 'scope'));
}
}
View
39 libraries/lithium/g11n/Catalog.php
@@ -27,25 +27,18 @@
*
* The class is able to aggregate data from different sources which allows to complement sparse
* data. Not all categories must be supported by an individual adapter.
- *
- * @todo Extend \lithium\core\Adaptable.
*/
-class Catalog extends \lithium\core\StaticObject {
+class Catalog extends \lithium\core\Adaptable {
protected static $_configurations = null;
- public static function __init() {
- static::$_configurations = new Collection();
- }
-
public static function config($config = null) {
- $default = array('adapter' => null, 'scope' => null);
+ $default = array('scope' => null);
if ($config) {
- $items = array_map(function($i) use ($default) { return $i + $default; }, $config);
- static::$_configurations = new Collection(compact('items'));
+ $config = array_map(function($i) use ($default) { return $i + $default; }, $config);
}
- return static::$_configurations;
+ return parent::config($config);
}
/**
@@ -148,28 +141,8 @@ public static function write($category, $data, $options = array()) {
return false;
}
- public static function clear() {
- static::__init();
- }
-
- public static function _adapter($name = null) {
- if (empty($name)) {
- $names = static::$_configurations->keys();
- if (empty($names)) {
- return;
- }
- $name = end($names);
- }
- if (!isset(static::$_configurations[$name])) {
- return;
- }
- if (is_string(static::$_configurations[$name]['adapter'])) {
- $config = static::$_configurations[$name];
- $class = Libraries::locate('adapter.g11n.catalog', $config['adapter']);
- $conf = array('adapter' => new $class($config)) + static::$_configurations[$name];
- static::$_configurations[$name] = $conf;
- }
- return static::$_configurations[$name]['adapter'];
+ public static function adapter($name) {
+ return static::_adapter('adapter.g11n.catalog', $name);
}
}
View
159 libraries/lithium/g11n/Message.php
@@ -9,124 +9,105 @@
namespace lithium\g11n;
use \lithium\core\Environment;
-use \lithium\util\String;
use \lithium\g11n\Locale;
use \lithium\g11n\Catalog;
/**
- * The `Message` class is concerned with aspects of the globalization of static message strings
- * throughout the framework.
+ * The `Message` class is concerned with an aspect of globalizing static message strings
+ * throughout the framework and applications. When referring to message globalization the
+ * phrase of ""translating a message" is widely used. This leads to the assumption that it's
+ * a single step process wheras it' a multi step one. A short description of each step is
+ * given here in order to help understanding the purpose of this class through the context
+ * of the process as a whole.
*
- * Often the phrase of "translating a message" is used for referring to globalization of messages
- * which leads to the false assumption that this is a single step, whereas it is a multi-step
- * process.
+ * 1. Marking messages as translateable. `$t()` and `$tn()` (implemented in the `View`
+ * class) are recognized as message marking and picked up by the extraction parser.
*
- * 1. Marking messages as translateable.
- * 2. Extracting marked messages, creating a message template.
- * 3. Translating messages, storing the translation.
- * 4. Retrieving the translation for a message.
+ * 2. Extracting marked messages. Messages can be extracted through the `g11n`
+ * command which in turn utilizes the `Catalog` class with the builtin `Code`
+ * adapter or other custom adapters which are concerned with extracting
+ * translatable content.
*
- * This class provides methods for the first and the last step of the process. The second
- * one is dealt with by the `Catalog` class (see the description for `Message::translate()` for
- * more information). The actual translation of messages by translators happens outside of the
- * framework using external applications.
+ * 3. Creating a message template from extracted messages. Templates are created
+ * by the `g11n` command using the `Catalog` class with an adapter for a format
+ * you prefer.
+ *
+ * 4. Translating messages. The actual translation of messages by translators
+ * happens outside using external applications.
+ *
+ * 5. Storing translated messages. Translations are most often stored by the external
+ * applications itself.
+ *
+ * 6. Retrieving the translation for a message. See description for `Message::translate()`.
+ *
+ * @see lithium\template\View
+ * @see lithium\g11n\Catalog
+ * @see lithium\console\command\G11n
+ * @see lithium\g11n\catalog\adapter\Code
*/
class Message extends \lithium\core\StaticObject {
/**
- * This method serves two purposes.
- *
- * For one it is used to mark transalateable messages, which can be extracted by the `Catalog`
- * class using the `Code` adapter for creating message template files. Since the marked messages
- * will be later translated by others it is important to keep a few best practices in mind.
- *
- * 1. Use entire English sentences (as it gives context).
- * 2. Split paragraphs into multiple messages.
- * 3. Instead of string concatenation utilize `String::insert()`-style format strings.
- * 4. Avoid to embed markup into the messages.
- * 5. Do not escape i.e. quotation marks where possible.
- *
- * The other purpose it serves is to return the translation of a message according to
- * the current or provided locale and (if applicable) plural form. The method can be used for
- * both single message or messages with a plural form. The provided message will be used as a
- * fall back if it isn't translateable. You may also use `String::insert()`-style place holders
- * within message strings and provide replacements as a separate option.
+ * Translates a message according to the current or provided locale
+ * and into it's correct plural form.
*
* Usage:
* {{{
* Message::translate('Mind the gap.');
- * Message::translate('house', array(
- * 'plural' => 'houses', 'count' => 23
- * ));
- * Message::translate('Your {:color} paintings are looking just great.', array(
- * 'replacements' => array('color' => 'silver'),
- * 'locale' => 'de'
- * ));
+ * Message::translate('house', array('count' => 23));
* }}}
*
- * @param string $singular Either a single or the singular form of the message.
- * @param array $options Allowed keys are:
- * - `'count'`: Used to determine the correct plural form.
- * - `'locale'`: The target locale, defaults to current locale.
- * - `'plural'`: Used as a fall back if needed.
- * - `'replacements'`: An array with replacements for place holders.
- * - `'scope'`: The scope of the message.
- * @return string
- *
- * @see lithium\console\command\g11n\Extract
- * @see lithium\g11n\catalog\adapter\Code
+ * @param string $id The id to use when looking up the translation.
+ * @param array $options Valid options are:
+ * - `'count'`: Used to determine the correct plural form.
+ * - `'locale'`: The target locale, defaults to current locale.
+ * - `'scope'`: The scope of the message.
+ * @return string|void The translation or `null` if none could be found.
+ * @filter
*/
- public static function translate($singular, $options = array()) {
- $defaults = array(
- 'plural' => null,
- 'count' => 1,
- 'replacements' => array(),
- // 'locale' => Environment::get('G11n.locale')
- 'locale' => 'de',
- 'scope' => null
- );
- extract($options + $defaults);
-
- if (!$translated = static::_translated($singular, $locale, $count, $scope)) {
- $translated = $count == 1 ? $singular : $plural;
- }
- return String::insert($translated, $replacements);
+ public static function translate($id, $options = array()) {
+ $params = compact('id', 'options');
+ return static::_filter(__METHOD__, $params, function($self, $params, $chain) {
+ return $self::invokeMethod('_translated', array($params['id'], $params['options']));
+ });
}
/**
- * Retrieves the translation for a message ID. Uses the `Catalog` class to
- * access translation data and determines the correct plural form (if applicable).
+ * Retrieves translations through the `Catalog` class by using `$id` as the lookup
+ * key and taking the current or - if specified - the provided locale as well as the
+ * scope into account. Hereupon the correct plural form is determined by passing the
+ * value of the `'count'` option to a closure.
*
- * @param string $id The message ID.
- * @param string $locale The target locale.
- * @param integer $count Used to determine the correct plural form.
- * @param string $scope The scope of the message ID.
- * @return string|void The translated message or `null` if `$singular` is not
- * translateable or a plural rule couldn't be found.
+ * @param string $id The lookup key.
+ * @param array $options Valid options are:
+ * - `'count'`: Used to determine the correct plural form.
+ * - `'locale'`: The target locale, defaults to current locale.
+ * - `'scope'`: The scope of the message.
+ * @return string|void The translation or `null` if none could be found or the plural
+ * form could not be determined.
* @see lithium\g11n\Catalog
* @todo Message pages need caching.
*/
- protected static function _translated($id, $locale, $count = null, $scope = null) {
- $result = Catalog::read('message.page', $locale, compact('scope'));
+ protected static function _translated($id, $options = array()) {
+ $defaults = array(
+ 'count' => 1,
+ // 'locale' => Environment::get('g11n.locale'),
+ 'locale' => null,
+ 'scope' => null
+ );
+ extract($options + $defaults);
+
+ $page = Catalog::read('message.page', $locale, compact('scope'));
+ $plural = Catalog::read('message.plural', $locale);
- if (empty($result[$locale][$id]['translated'])) {
+ if (empty($page[$locale][$id]['translated']) || !isset($plural[$locale])) {
return null;
}
- $translated = $result[$locale][$id]['translated'];
-
- if (isset($count)) {
- $result = Catalog::read('message.plural', $locale);
-
- if (!isset($result[$locale])) {
- return null;
- }
- $key = $result[$locale]($count);
+ $translated = $page[$locale][$id]['translated'];
+ $key = $plural[$locale]($count);
- if (isset($translated[$key])) {
- return $translated[$key];
- }
- } else {
- return array_shift($translated);
+ if (isset($translated[$key])) {
+ return $translated[$key];
}
}
}
View
85 libraries/lithium/g11n/catalog/adapter/Code.php
@@ -102,29 +102,32 @@ public function read($category, $locale, $scope) {
public function write($category, $locale, $scope, $data) {}
/**
- * Parses a PHP file for translateable strings wrapped in `$t()` calls.
+ * Parses a PHP file for messages marked as translatable. Recognized as message
+ * marking are `$t()` and `$tn()` which are implemented in the `View` class. This
+ * is a rather simple and stupid parser but also fast and easy to grasp. It doesn't
+ * actively attempt to detect and work around syntax errors in marker functions.
*
* @param string $file Absolute path to a PHP file.
* @return array
- * @todo How should invalid entries be handled?
+ * @see lithium\template\View
*/
protected function _parsePhp($file) {
$contents = file_get_contents($file);
- if (strpos($contents, '$t(') === false) {
- return array();
- }
-
$defaults = array(
'singularId' => null,
'pluralId' => null,
'open' => false,
- 'concat' => false,
+ 'position' => 0,
'occurrence' => array('file' => $file, 'line' => null)
);
extract($defaults);
$data = array();
+ if (strpos($contents, '$t(') === false && strpos($contents, '$tn(') == false) {
+ return $data;
+ }
+
$tokens = token_get_all($contents);
unset($contents);
@@ -133,28 +136,29 @@ protected function _parsePhp($file) {
$token = array(0 => null, 1 => $token, 2 => null);
}
- if (!$open) {
- if ($token[1] === '$t' && isset($tokens[$key + 1]) && $tokens[$key + 1] === '(') {
- $open = true;
- $occurrence['line'] = $token[2];
+ if ($open) {
+ if ($position >= ($open === 'singular' ? 1 : 2)) {
+ $this->_mergeMessageItem($data, array(
+ 'singularId' => $singularId,
+ 'pluralId' => $pluralId,
+ 'occurrences' => array($occurrence),
+ ));
+ extract($defaults, EXTR_OVERWRITE);
+ } elseif ($token[0] === T_CONSTANT_ENCAPSED_STRING) {
+ $type = isset($singularId) ? 'pluralId' : 'singularId';
+ $$type = $token[1];
+ $position++;
}
} else {
- if ($token[1] === '.') {
- $concat = true;
- } elseif ($token[1] === ',') {
- $concat = false;
- } elseif ($token[0] === T_CONSTANT_ENCAPSED_STRING && !isset($pluralId)) {
- $type = isset($singularId) ? 'pluralId' : 'singularId';
- $$type = ($concat ? $$type : null) . $this->_formatMessage($token[1]);
- } elseif ($token[0] !== T_WHITESPACE && $token[1] !== '(') {
- if (isset($singularId)) {
- $this->_mergeMessageItem($data, array(
- 'singularId' => $singularId,
- 'pluralId' => $pluralId,
- 'occurrences' => array($occurrence),
- ));
+ if (isset($tokens[$key + 1]) && $tokens[$key + 1] === '(') {
+ if ($token[1] === '$t') {
+ $open = 'singular';
+ } elseif ($token[1] === '$tn') {
+ $open = 'plural';
+ } else {
+ continue;
}
- extract($defaults, EXTR_OVERWRITE);
+ $occurrence['line'] = $token[2];
}
}
}
@@ -162,23 +166,24 @@ protected function _parsePhp($file) {
}
/**
- * Formats a string to be added as a message.
+ * Merges a message item into given data and removes quotation marks
+ * from the beginning and end of message strings.
*
- * @param string $string
- * @return string
+ * @param array $data Data to merge item into.
+ * @param array $item Item to merge into $data.
+ * @return void
+ * @see lithium\g11n\catalog\adapter\Base::_mergeMessageItem()
*/
- function _formatMessage($string) {
- $quote = substr($string, 0, 1);
- $string = substr($string, 1, -1);
-
- if ($quote === '"') {
- $string = stripcslashes($string);
- } else {
- $string = strtr($string, array("\\'" => "'", "\\\\" => "\\"));
+ protected function _mergeMessageItem(&$data, $item) {
+ $fields = array('singularId', 'pluralId');
+
+ foreach ($fields as $field) {
+ if (isset($item[$field])) {
+ $item[$field] = substr($item[$field], 1, -1);
+ }
}
- $string = str_replace("\r\n", "\n", $string);
- return addcslashes($string, "\0..\37\\\"");
- }
+ return parent::_mergeMessageItem($data, $item);
+ }
}
?>
View
94 libraries/lithium/g11n/catalog/adapter/Gettext.php
@@ -18,7 +18,8 @@
*
* The adapter expects a directory configured by the path options to be structured
* according to the following example.
- *{{{
+ *
+ * {{{
* | - `<path>`: This is the configured path.
* | - `<locale>`: The directory for the well-formed <locale> i.e `'fr' or `'en_US'`.
* | | - `LC_MESSAGES`: The directory for the message category.
@@ -28,7 +29,8 @@
* | | - `<scope>.mo`: The MO file for <scope>.
* | - `message_default.pot`: The message template.
* | - `message_<scope>.pot`: The message template for <scope>.
- *}}}
+ * }}}
+ *
* @see lithium\g11n\Locale
* @link http://php.net/setlocale
*/
@@ -84,9 +86,10 @@ public function read($category, $locale, $scope) {
foreach ($files as $file) {
$method = '_parse' . ucfirst(pathinfo($file, PATHINFO_EXTENSION));
- if (!$stream = fopen($file, 'rb')) {
+ if (!is_readable($file)) {
continue;
}
+ $stream = fopen($file, 'rb');
$data = $this->invokeMethod($method, array($stream));
fclose($stream);
@@ -184,15 +187,15 @@ protected function _parsePo($stream) {
));
extract($defaults, EXTR_OVERWRITE);
}
- $singularId = stripcslashes($matches[1]);
+ $singularId = $matches[1];
} elseif (preg_match('/^msgid_plural\s"(.+)"$/', $line, $matches)) {
- $pluralId = stripcslashes($matches[1]);
+ $pluralId = $matches[1];
} elseif (preg_match('/^msgstr\s"(.+)"$/', $line, $matches)) {
- $translated[0] = stripcslashes($matches[1]);
+ $translated[0] = $matches[1];
} elseif (preg_match('/^msgstr\[(\d+)\]\s"(.+)"$/', $line, $matches)) {
- $translated[$matches[1]] = stripcslashes($matches[2]);
+ $translated[$matches[1]] = $matches[2];
} elseif ($translated && preg_match('/^"(.+)"$/', $line, $matches)) {
- $translated[key($translated)] .= stripcslashes($matches[1]);
+ $translated[key($translated)] .= $matches[1];
}
}
$this->_mergeMessageItem($data, compact(
@@ -353,25 +356,26 @@ protected function _compilePo($stream, $data, $meta) {
$item = $this->_formatMessageItem($key, $item);
foreach ($item['occurrences'] as $occurrence) {
- $output[] = '#: ' . $occurrence['file'] . ':' . $occurrence['line'];
+ $output[] = "#: {$occurrence['file']}:{$occurrence['line']}";
}
foreach ($item['comments'] as $comment) {
- $output[] = '#. ' . $comment;
+ $output[] = "#. {$comment}";
}
if ($item['fuzzy']) {
- $output[] = '#, fuzzy';
+ $output[] = "#, fuzzy";
}
- $output[] = 'msgid "' . $item['singularId'] . '"';
+ $output[] = "msgid \"{$item['singularId']}\"";
- if (!isset($item['pluralId'])) {
- $output[] = 'msgstr "' . $item['translated'] . '"';
- } else {
- $output[] = 'msgid_plural "' . $item['pluralId'] . '"';
+ if (isset($item['pluralId'])) {
+ $output[] = "msgid_plural \"{$item['pluralId']}\"";
- foreach ($item['translated'] as $key => $value) {
- $output[] = 'msgstr[' . $key . '] "' . $value . '"';
+ foreach ($item['translated'] ?: array(null, null) as $key => $value) {
+ $output[] = "msgstr[{$key}] \"{$value}\"";
}
+ } else {
+ $value = array_pop($item['translated']);
+ $output[] = "msgstr \"{$value}\"";
}
$output[] = '';
$output = implode("\n", $output) . "\n";
@@ -401,6 +405,60 @@ protected function _compilePot($stream, $data, $meta) {
* @return void
*/
protected function _compileMo($stream, $data, $meta) {}
+
+ /**
+ * Formats a message item if neccessary and escapes fields.
+ *
+ * @param string $key The potential message ID.
+ * @param string|array $value The message value.
+ * @return array Message item formatted into internal/verbose format.
+ * @see lithium\g11n\catalog\adapter\Base::_formatMessageItem()
+ */
+ protected function _formatMessageItem($key, $value) {
+ $escape = function ($value) use (&$escape) {
+ if (is_array($value)) {
+ return array_map($escape, $value);
+ }
+ $value = strtr($value, array("\\'" => "'", "\\\\" => "\\"));
+ $value = str_replace("\r\n", "\n", $value);
+ $value = addcslashes($value, "\0..\37\\\"");
+ return $value;
+ };
+ $fields = array('singularId', 'pluralId', 'translated');
+ $item = parent::_formatMessageItem($key, $value);
+
+ foreach ($fields as $field) {
+ if (isset($item[$field])) {
+ $item[$field] = $escape($item[$field]);
+ }
+ }
+ return $item;
+ }
+
+ /**
+ * Merges a message item into given data and unescapes fields.
+ *
+ * @param array $data Data to merge item into.
+ * @param array $item Item to merge into $data.
+ * @return void
+ * @see lithium\g11n\catalog\adapter\Base::_mergeMessageItem()
+ */
+ protected function _mergeMessageItem(&$data, $item) {
+ $unescape = function ($value) use (&$unescape) {
+ if (is_array($value)) {
+ return array_map($unescape, $value);
+ }
+ return stripcslashes($value);
+ };
+ $fields = array('singularId', 'pluralId', 'translated');
+
+ foreach ($fields as $field) {
+ if (isset($item[$field])) {
+ $item[$field] = $unescape($item[$field]);
+ }
+ }
+ return parent::_mergeMessageItem($data, $item);
+ }
}
?>
View
3,998 libraries/lithium/g11n/resources/message_lithium_docs.pot
3,998 additions, 0 deletions not shown
View
14 libraries/lithium/template/View.php
@@ -9,7 +9,6 @@
namespace lithium\template;
use \RuntimeException;
-use \lithium\util\String;
use \lithium\core\Libraries;
use \lithium\g11n\Message;
@@ -60,10 +59,17 @@ protected function _init() {
$h = function($data) use (&$h) {
return is_array($data) ? array_map($h, $data) : htmlspecialchars((string)$data);
};
- $t = function($singular, $options = array()) {
- return Message::translate($singular, $options);
+ $t = function($message, $options = array()) {
+ return Message::translate($message, $options + array(
+ 'default' => $message
+ ));
};
- $this->outputFilters += compact('h', 't');
+ $tn = function($message1, $message2, $count, $options = array()) {
+ return Message::translate($message1, $options + compact('count') + array(
+ 'default' => $count == 1 ? $message1 : $message2
+ ));
+ };
+ $this->outputFilters += compact('h', 't', 'tn');
}
public function render($type, $data = array(), $options = array()) {
View
28 libraries/lithium/tests/cases/g11n/CatalogTest.php
@@ -17,38 +17,18 @@ class CatalogTest extends \lithium\test\Unit {
public function setUp() {
$this->_backups['catalogConfig'] = Catalog::config()->to('array');
- Catalog::clear();
+ Catalog::reset();
Catalog::config(array(
'runtime' => array('adapter' => new Memory())
));
}
public function tearDown() {
- Catalog::clear();
+ Catalog::reset();
Catalog::config($this->_backups['catalogConfig']);
}
/**
- * Tests configuration.
- *
- * @return void
- */
- public function testConfig() {}
-
- /**
- * Tests if configurations are cleared.
- *
- * @return void
- */
- public function testClear() {
- $this->assertTrue(Catalog::config()->count());
- Catalog::clear();
- $this->assertFalse(Catalog::config()->count());
- }
-
- public function testDescribe() {}
-
- /**
* Tests for values returned by `read()`.
*
* @return void
@@ -322,7 +302,7 @@ public function testWriteReadWithScope() {
* @return void
*/
public function testWriteReadMergeConfigurations() {
- Catalog::clear();
+ Catalog::reset();
Catalog::config(array(
'runtime0' => array('adapter' => new Memory()),
'runtime1' => array('adapter' => new Memory())
@@ -343,7 +323,7 @@ public function testWriteReadMergeConfigurations() {
);
$this->assertEqual($expected, $result);
- Catalog::clear();
+ Catalog::reset();
Catalog::config(array(
'runtime0' => array('adapter' => new Memory()),
'runtime1' => array('adapter' => new Memory())
View
20 libraries/lithium/tests/cases/g11n/MessageTest.php
@@ -17,30 +17,22 @@ class MessageTest extends \lithium\test\Unit {
protected $_backups = array();
- protected $_locale;
-
- protected $_connection;
-
public function setUp() {
- // $this->_backups['locale'] = Environment::get('G11n.locale');
$this->_backups['catalogConfig'] = Catalog::config()->to('array');
- Catalog::clear();
+ Catalog::reset();
Catalog::config(array(
'runtime' => array('adapter' => new Memory())
));
}
public function tearDown() {
- Catalog::clear();
+ Catalog::reset();
Catalog::config($this->_backups['catalogConfig']);
- // Environment::set('G11n.locale', $this->_backup['locale']);
}
public function testTranslate() {
- // Environment::set('G11n.locale', 'de');
-
$data = array(
- 'de' => function($n) { return $n == 1 ? 0 : 1; }
+ 'root' => function($n) { return $n == 1 ? 0 : 1; }
);
Catalog::write('message.plural', $data, array('name' => 'runtime'));
@@ -52,15 +44,15 @@ public function testTranslate() {
Catalog::write('message.page', $data, array('name' => 'runtime'));
$expected = 'Kuchen';
- $result = Message::translate('lithium');
+ $result = Message::translate('lithium', array('locale' => 'de'));
$this->assertEqual($expected, $result);
$expected = 'Haus';
- $result = Message::translate('house');
+ $result = Message::translate('house', array('locale' => 'de'));
$this->assertEqual($expected, $result);
$expected = 'Häuser';
- $result = Message::translate('house', array('count' => 5));
+ $result = Message::translate('house', array('locale' => 'de', 'count' => 5));
$this->assertEqual($expected, $result);
}
}
View
173 libraries/lithium/tests/cases/g11n/catalog/adapter/CodeTest.php
@@ -11,25 +11,34 @@
use \lithium\g11n\catalog\adapter\Code;
if (false) {
- $t('message 1');
- $t('message 2', array('a' => 'b'));
+ $t('simple 1');
+
+ $t('options 1', null, array('locale' => 'en'));
+
+ $t('replace 1 {:a}', array('a' => 'b'));
$t($test['invalid']);
$t(32203);
- $t('message 3', $test['invalid']);
- $t('message 4', 32203);
+ $t('invalid 1', $test['invalid']);
+ $t('invalid 2', 32203);
+ $t('invalid 3', 'invalid 3b');
- $t('message\n5');
- $t("message\n6");
- $t("message\r\n7");
- $t('message
- 8');
+ $t('escaping\n1');
+ $t("escaping\n2");
+ $t("escaping\r\n3");
+ $t('escaping
+ 4');
- $t('singular 1', 'plural 1');
- $t('singular 2', 'plural 2', array('a' => 'b'));
+ $tn('singular simple 1', 'plural simple 1', 3);
+ $tn('singular simple 2', 'plural simple 2');
$t('mixed 1');
- $t('mixed 1', 'plural 3');
+ $tn('mixed 1', 'plural mixed 1', 3);
+
+ $t('mixed 2');
+ $tn('mixed 2', 'plural mixed 2', 3);
+ $t('mixed 2');
+ $t('plural mixed 2');
}
class CodeTest extends \lithium\test\Unit {
@@ -41,50 +50,128 @@ public function setUp() {
$this->adapter = new Code(compact('path'));
}
- /**
- * Tests message string parsing, invalid values must be skipped.
- *
- * @return void
- */
- public function testReadMessageTemplate() {
- $result = $this->adapter->read('message.template', 'root', null);
+ public function testReadMessageTemplateTSimple() {
+ $results = $this->adapter->read('message.template', 'root', null);
+
+ $expected = 'simple 1';
+ $result = $results['simple 1']['singularId'];
+ $this->assertEqual($expected, $result);
+
+ $result = $results['simple 1']['pluralId'];
+ $this->assertFalse($result);
+ }
+
+ public function testReadMessageTemplateTOptions() {
+ $results = $this->adapter->read('message.template', 'root', null);
+
+ $expected = 'options 1';
+ $result = $results['options 1']['singularId'];
+ $this->assertEqual($expected, $result);
+
+ $result = $results['options 1']['pluralId'];
+ $this->assertFalse($result);
+ }
+
+ public function testReadMessageTemplateTReplace() {
+ $results = $this->adapter->read('message.template', 'root', null);
+
+ $expected = 'replace 1 {:a}';
+ $result = $results['replace 1 {:a}']['singularId'];
+ $this->assertEqual($expected, $result);
+
+ $result = $results['replace 1 {:a}']['pluralId'];
+ $this->assertFalse($result);
+ }
+
+ public function testReadMessageTemplateTInvalid() {
+ $results = $this->adapter->read('message.template', 'root', null);
+
+ $result = isset($results['32203']);
+ $this->assertFalse($result);
+
+ $result = isset($results[32203]);
+ $this->assertFalse($result);
+
+ $expected = 'invalid 1';
+ $result = $results['invalid 1']['singularId'];
+ $this->assertEqual($expected, $result);
+
+ $result = $results['invalid 1']['pluralId'];
+ $this->assertFalse($result);
+
+ $expected = 'invalid 2';
+ $result = $results['invalid 2']['singularId'];
+ $this->assertEqual($expected, $result);
+
+ $result = $results['invalid 2']['pluralId'];
+ $this->assertFalse($result);
+
+ $expected = 'invalid 3';
+ $result = $results['invalid 3']['singularId'];
+ $this->assertEqual($expected, $result);
+
+ $result = $results['invalid 3']['pluralId'];
+ $this->assertFalse($result);
+ }
+
+ public function testReadMessageTemplateTNoEscaping() {
+ $results = $this->adapter->read('message.template', 'root', null);
- /* Simple */
+ $expected = 'escaping\n1';
+ $result = $results['escaping\n1']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertEqual('message 1', $result['message 1']['singularId']);
- $this->assertFalse($result['message 1']['pluralId']);
+ $expected = 'escaping\n2';
+ $result = $results['escaping\n2']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertEqual('message 2', $result['message 2']['singularId']);
- $this->assertFalse($result['message 2']['pluralId']);
+ $expected = 'escaping\r\n3';
+ $result = $results['escaping\r\n3']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertFalse(isset($result['32203']));
- $this->assertFalse(isset($result[32203]));
+ $expected = "escaping\n\t4";
+ $result = $results["escaping\n\t4"]['singularId'];
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testReadMessageTemplateTnSimple() {
+ $results = $this->adapter->read('message.template', 'root', null);
- $this->assertEqual('message 3', $result['message 3']['singularId']);
- $this->assertFalse($result['message 3']['pluralId']);
+ $expected = 'singular simple 1';
+ $result = $results['singular simple 1']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertEqual('message 4', $result['message 4']['singularId']);
- $this->assertFalse($result['message 4']['pluralId']);
+ $expected = 'plural simple 1';
+ $result = $results['singular simple 1']['pluralId'];
+ $this->assertEqual($expected, $result);
- /* Escaping */
+ $expected = 'singular simple 2';
+ $result = $results['singular simple 2']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertEqual('message\\\n5', $result['message\\\n5']['singularId']);
- $this->assertEqual('message\n6', $result['message\n6']['singularId']);
- $this->assertEqual('message\n7', $result['message\n7']['singularId']);
- $this->assertEqual('message\n\t8', $result['message\n\t8']['singularId']);
+ $expected = 'plural simple 2';
+ $result = $results['singular simple 2']['pluralId'];
+ $this->assertEqual($expected, $result);
+ }
- /* Plurals */
+ public function testReadMessageTemplateTnT() {
+ $results = $this->adapter->read('message.template', 'root', null);
- $this->assertEqual('singular 1', $result['singular 1']['singularId']);
- $this->assertEqual('plural 1', $result['singular 1']['pluralId']);
+ $expected = 'mixed 1';
+ $result = $results['mixed 1']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertEqual('singular 2', $result['singular 2']['singularId']);
- $this->assertEqual('plural 2', $result['singular 2']['pluralId']);
+ $expected = 'plural mixed 1';
+ $result = $results['mixed 1']['pluralId'];
+ $this->assertEqual($expected, $result);
- /* Merging simple and plural message strings */
+ $expected = 'mixed 2';
+ $result = $results['mixed 2']['singularId'];
+ $this->assertEqual($expected, $result);
- $this->assertEqual('mixed 1', $result['mixed 1']['singularId'], 'mixed 1');
- $this->assertEqual('plural 3', $result['mixed 1']['pluralId'], 'plural 3');
+ $expected = 'plural mixed 2';
+ $result = $results['mixed 2']['pluralId'];
+ $this->assertEqual($expected, $result);
}
}
View
2  libraries/lithium/tests/cases/g11n/catalog/adapter/GettextTest.php
@@ -40,7 +40,7 @@ public function tearDown() {
rmdir($this->_path);
}
- function testWriteReadMessageTemplate() {
+ public function testWriteReadMessageTemplate() {
$data = array(
'singular 1' => array(
'singularId' => 'singular 1',
View
40 libraries/lithium/tests/cases/template/ViewTest.php
@@ -10,6 +10,8 @@
use \lithium\template\View;
use \lithium\template\view\adapter\Simple;
+use \lithium\g11n\Catalog;
+use \lithium\g11n\catalog\adapter\Memory;
class TestViewClass extends \lithium\template\View {
@@ -40,12 +42,48 @@ public function testInitializationWithBadClasses() {
new View(array('renderer' => 'Badness'));
}
- public function testOutputFilters() {
+ public function testEscapeOutputFilter() {
$h = $this->_view->outputFilters['h'];
$expected = '&lt;p&gt;Foo, Bar &amp; Baz&lt;/p&gt;';
$result = $h('<p>Foo, Bar & Baz</p>');
$this->assertEqual($expected, $result);
}
+
+ public function testTranslationOutputFilters() {
+ $backup = Catalog::config()->to('array');
+ Catalog::reset();
+ Catalog::config(array(
+ 'runtime' => array('adapter' => new Memory())
+ ));
+ $data = array(
+ 'root' => function($n) { return $n == 1 ? 0 : 1; }
+ );
+ Catalog::write('message.plural', $data, array('name' => 'runtime'));
+
+ $data = array(
+ 'de' => array(
+ 'house' => array('Haus', 'Häuser')
+ ));
+ Catalog::write('message.page', $data, array('name' => 'runtime'));
+
+ $t = $this->_view->outputFilters['t'];
+ $tn = $this->_view->outputFilters['tn'];
+
+ $expected = 'Haus';
+ $result = $t('house', array('locale' => 'de'));
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Haus';
+ $result = $tn('house', 'houses', 1, array('locale' => 'de'));
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Häuser';
+ $result = $tn('house', 'houses', 3, array('locale' => 'de'));
+ $this->assertEqual($expected, $result);
+
+ Catalog::reset();
+ Catalog::config($backup);
+ }
}
?>
Please sign in to comment.
Something went wrong with that request. Please try again.