Commit db3bfd09 by Qiang Xue

Merge pull request #528 from klimov-paul/message-command

Compose Message Command
parents d19446ba 014053f0
...@@ -9,23 +9,37 @@ ...@@ -9,23 +9,37 @@
namespace yii\console\controllers; namespace yii\console\controllers;
use yii\console\Controller; use yii\console\Controller;
use yii\console\Exception;
use yii\helpers\FileHelper;
/** /**
* This command extracts messages to be translated from source files. * This command extracts messages to be translated from source files.
* The extracted messages are saved as PHP message source files * The extracted messages are saved as PHP message source files
* under the specified directory. * under the specified directory.
* *
* Usage:
* 1. Create a configuration file using 'template' action:
* yii message/template /path/to/myapp/messages/config.php
* 2. Edit the created config file, adjusting it for your web application needs.
* 3. Run the 'generate' action, using created config:
* yii message /path/to/myapp/messages/config.php
*
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class MessageController extends Controller class MessageController extends Controller
{ {
/** /**
* @var string controller default action ID.
*/
public $defaultAction = 'generate';
/**
* Searches for messages to be translated in the specified * Searches for messages to be translated in the specified
* source files and compiles them into PHP arrays as message source. * source files and compiles them into PHP arrays as message source.
* *
* @param string $config the path of the configuration file. You can find * @param string $config the path of the configuration file. You can find
* an example in framework/messages/config.php. * an example in framework/messages/config.php.
* @throws \yii\console\Exception on failure.
* *
* The file can be placed anywhere and must be a valid PHP script which * The file can be placed anywhere and must be a valid PHP script which
* returns an array of name-value pairs. Each name-value pair represents * returns an array of name-value pairs. Each name-value pair represents
...@@ -48,35 +62,36 @@ class MessageController extends Controller ...@@ -48,35 +62,36 @@ class MessageController extends Controller
* directory 'sourcePath/a/b'. * directory 'sourcePath/a/b'.
* - translator: the name of the function for translating messages. * - translator: the name of the function for translating messages.
* Defaults to 'Yii::t'. This is used as a mark to find messages to be * Defaults to 'Yii::t'. This is used as a mark to find messages to be
* translated. * translated. Accepts both string for single function name or array for
* multiple function names.
* - overwrite: if message file must be overwritten with the merged messages. * - overwrite: if message file must be overwritten with the merged messages.
* - removeOld: if message no longer needs translation it will be removed, * - removeOld: if message no longer needs translation it will be removed,
* instead of being enclosed between a pair of '@@' marks. * instead of being enclosed between a pair of '@@' marks.
* - sort: sort messages by key when merging, regardless of their translation * - sort: sort messages by key when merging, regardless of their translation
* state (new, obsolete, translated.) * state (new, obsolete, translated.)
*/ */
public function actionIndex($config) public function actionGenerate($config)
{ {
if (!is_file($config)) { if (!is_file($config)) {
$this->usageError("the configuration file {$config} does not exist."); throw new Exception("the configuration file {$config} does not exist.");
} }
$config = require_once($config); $config = require($config);
$translator='Yii::t'; $translator = 'Yii::t';
extract($config); extract($config);
if (!isset($sourcePath, $messagePath, $languages)) { if (!isset($sourcePath, $messagePath, $languages)) {
$this->usageError('The configuration file must specify "sourcePath", "messagePath" and "languages".'); throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".');
} }
if (!is_dir($sourcePath)) { if (!is_dir($sourcePath)) {
$this->usageError("The source path $sourcePath is not a valid directory."); throw new Exception("The source path {$sourcePath} is not a valid directory.");
} }
if (!is_dir($messagePath)) { if (!is_dir($messagePath)) {
$this->usageError("The message path $messagePath is not a valid directory."); throw new Exception("The message path {$messagePath} is not a valid directory.");
} }
if (empty($languages)) { if (empty($languages)) {
$this->usageError("Languages cannot be empty."); throw new Exception("Languages cannot be empty.");
} }
if (!isset($overwrite)) { if (!isset($overwrite)) {
...@@ -96,7 +111,7 @@ class MessageController extends Controller ...@@ -96,7 +111,7 @@ class MessageController extends Controller
if (isset($exclude)) { if (isset($exclude)) {
$options['exclude'] = $exclude; $options['exclude'] = $exclude;
} }
$files = CFileHelper::findFiles(realpath($sourcePath), $options); $files = FileHelper::findFiles(realpath($sourcePath), $options);
$messages = array(); $messages = array();
foreach ($files as $file) { foreach ($files as $file) {
...@@ -126,18 +141,23 @@ class MessageController extends Controller ...@@ -126,18 +141,23 @@ class MessageController extends Controller
{ {
echo "Extracting messages from $fileName...\n"; echo "Extracting messages from $fileName...\n";
$subject = file_get_contents($fileName); $subject = file_get_contents($fileName);
$n = preg_match_all(
'/\b' . $translator . '\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s',
$subject, $matches, PREG_SET_ORDER);
$messages = array(); $messages = array();
for ($i = 0; $i < $n; ++$i) { if (!is_array($translator)) {
if (($pos = strpos($matches[$i][1], '.')) !== false) { $translator = array($translator);
$category=substr($matches[$i][1], $pos + 1, -1); }
} else { foreach ($translator as $currentTranslator) {
$category=substr($matches[$i][1], 1, -1); $n = preg_match_all(
'/\b' . $currentTranslator . '\s*\(\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*,\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)")\s*[,\)]/s',
$subject, $matches, PREG_SET_ORDER);
for ($i = 0; $i < $n; ++$i) {
if (($pos = strpos($matches[$i][1], '.')) !== false) {
$category = substr($matches[$i][1], $pos + 1, -1);
} else {
$category = substr($matches[$i][1], 1, -1);
}
$message = $matches[$i][2];
$messages[$category][] = eval("return $message;"); // use eval to eliminate quote escape
} }
$message = $matches[$i][2];
$messages[$category][] = eval("return $message;"); // use eval to eliminate quote escape
} }
return $messages; return $messages;
} }
...@@ -165,7 +185,7 @@ class MessageController extends Controller ...@@ -165,7 +185,7 @@ class MessageController extends Controller
$merged = array(); $merged = array();
$untranslated = array(); $untranslated = array();
foreach ($messages as $message) { foreach ($messages as $message) {
if (!empty($translated[$message])) { if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) {
$merged[$message] = $translated[$message]; $merged[$message] = $translated[$message];
} else { } else {
$untranslated[] = $message; $untranslated[] = $message;
...@@ -209,7 +229,7 @@ class MessageController extends Controller ...@@ -209,7 +229,7 @@ class MessageController extends Controller
/** /**
* Message translations. * Message translations.
* *
* This file is automatically generated by 'yii message' command. * This file is automatically generated by 'yii {$this->id}' command.
* It contains the localizable messages extracted from source code. * It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages. * You may modify this file by translating the extracted messages.
* *
...@@ -221,11 +241,51 @@ class MessageController extends Controller ...@@ -221,11 +241,51 @@ class MessageController extends Controller
* Message string can be used with plural forms format. Check i18n section * Message string can be used with plural forms format. Check i18n section
* of the guide for details. * of the guide for details.
* *
* NOTE, this file must be saved in UTF-8 encoding. * NOTE: this file must be saved in UTF-8 encoding.
*/ */
return $array; return $array;
EOD; EOD;
file_put_contents($fileName, $content); file_put_contents($fileName, $content);
} }
/**
* Creates template of configuration file for [[actionGenerate]].
* @param string $configFile output file name.
* @throws \yii\console\Exception on failure.
*/
public function actionTemplate($configFile)
{
$template = <<<EOD
<?php
/**
* Configuration file for the "yii {$this->id}" console command.
*/
return array(
'sourcePath' => __DIR__,
'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
'languages' => array(),
'fileTypes' => array('php'),
'overwrite' => true,
'exclude' => array(
'.svn',
'.gitignore',
'.gitkeep',
'.hgignore',
'.hgkeep',
'/messages',
),
);
EOD;
if (file_exists($configFile)) {
if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) {
return;
}
}
if (!file_put_contents($configFile, $template)) {
throw new Exception("Unable to write template file '{$configFile}'.");
} else {
echo "Configuration file template created at '{$configFile}'.\n\n";
}
}
} }
<?php
use yiiunit\TestCase;
use yii\console\controllers\MessageController;
/**
* Unit test for [[\yii\console\controllers\MessageController]].
* @see MessageController
*/
class MessageControllerTest extends TestCase
{
protected $sourcePath = '';
protected $messagePath = '';
protected $configFileName = '';
public function setUp()
{
$this->sourcePath = Yii::getAlias('@yiiunit/runtime/test_source');
$this->createDir($this->sourcePath);
if (!file_exists($this->sourcePath)) {
$this->markTestIncomplete('Unit tests runtime directory should have writable permissions!');
}
$this->messagePath = Yii::getAlias('@yiiunit/runtime/test_messages');
$this->createDir($this->messagePath);
$this->configFileName = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . 'message_controller_test_config.php';
}
public function tearDown()
{
$this->removeDir($this->sourcePath);
$this->removeDir($this->messagePath);
if (file_exists($this->configFileName)) {
unlink($this->configFileName);
}
}
/**
* Creates directory.
* @param $dirName directory full name
*/
protected function createDir($dirName)
{
if (!file_exists($dirName)) {
mkdir($dirName, 0777, true);
}
}
/**
* Removes directory.
* @param $dirName directory full name
*/
protected function removeDir($dirName)
{
if (!empty($dirName) && file_exists($dirName)) {
$this->removeFileSystemObject($dirName);
}
}
/**
* Removes file system object: directory or file.
* @param string $fileSystemObjectFullName file system object full name.
*/
protected function removeFileSystemObject($fileSystemObjectFullName)
{
if (!is_dir($fileSystemObjectFullName)) {
unlink($fileSystemObjectFullName);
} else {
$dirHandle = opendir($fileSystemObjectFullName);
while (($fileSystemObjectName = readdir($dirHandle)) !== false) {
if ($fileSystemObjectName==='.' || $fileSystemObjectName==='..') {
continue;
}
$this->removeFileSystemObject($fileSystemObjectFullName . DIRECTORY_SEPARATOR . $fileSystemObjectName);
}
closedir($dirHandle);
rmdir($fileSystemObjectFullName);
}
}
/**
* Creates test message controller instance.
* @return MessageController message command instance.
*/
protected function createMessageController()
{
$module = $this->getMock('yii\\base\\Module', array('fake'), array('console'));
$messageController = new MessageController('message', $module);
$messageController->interactive = false;
return $messageController;
}
/**
* Emulates running of the message controller action.
* @param string $actionId id of action to be run.
* @param array $args action arguments.
* @return string command output.
*/
protected function runMessageControllerAction($actionId, array $args = array())
{
$controller = $this->createMessageController();
ob_start();
ob_implicit_flush(false);
$params = array(
\yii\console\Request::ANONYMOUS_PARAMS => $args
);
$controller->run($actionId, $params);
return ob_get_clean();
}
/**
* Creates message command config file at {@link configFileName}
* @param array $config message command config.
*/
protected function composeConfigFile(array $config)
{
if (file_exists($this->configFileName)) {
unlink($this->configFileName);
}
$fileContent = '<?php return ' . var_export($config, true) . ';';
file_put_contents($this->configFileName, $fileContent);
}
/**
* Creates source file with given content
* @param string $content file content
* @param string|null $name file self name
*/
protected function createSourceFile($content, $name = null)
{
if (empty($name)) {
$name = md5(uniqid()) . '.php';
}
file_put_contents($this->sourcePath . DIRECTORY_SEPARATOR . $name, $content);
}
/**
* Creates message file with given messages.
* @param string $name file name
* @param array $messages messages.
*/
protected function createMessageFile($name, array $messages = array())
{
$fileName = $this->messagePath . DIRECTORY_SEPARATOR . $name;
if (file_exists($fileName)) {
unlink($fileName);
} else {
$dirName = dirname($fileName);
if (!file_exists($dirName)) {
mkdir($dirName, 0777, true);
}
}
$fileContent = '<?php return ' . var_export($messages, true) . ';';
file_put_contents($fileName, $fileContent);
}
// Tests:
public function testActionTemplate()
{
$configFileName = $this->configFileName;
$this->runMessageControllerAction('template', array($configFileName));
$this->assertTrue(file_exists($configFileName), 'Unable to create config file template!');
}
public function testConfigFileNotExist()
{
$this->setExpectedException('yii\\console\\Exception');
$this->runMessageControllerAction('generate', array('not_existing_file.php'));
}
public function testCreateTranslation()
{
$language = 'en';
$category = 'test_category';
$message = 'test message';
$sourceFileContent = "Yii::t('{$category}', '{$message}')";
$this->createSourceFile($sourceFileContent);
$this->composeConfigFile(array(
'languages' => array($language),
'sourcePath' => $this->sourcePath,
'messagePath' => $this->messagePath,
));
$this->runMessageControllerAction('generate', array($this->configFileName));
$this->assertTrue(file_exists($this->messagePath . DIRECTORY_SEPARATOR . $language), 'No language dir created!');
$messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php';
$this->assertTrue(file_exists($messageFileName), 'No message file created!');
$messages = require($messageFileName);
$this->assertTrue(is_array($messages), 'Unable to compose messages!');
$this->assertTrue(array_key_exists($message, $messages), 'Source message is missing!');
}
/**
* @depends testCreateTranslation
*/
public function testNothingNew()
{
$language = 'en';
$category = 'test_category';
$message = 'test message';
$sourceFileContent = "Yii::t('{$category}', '{$message}')";
$this->createSourceFile($sourceFileContent);
$this->composeConfigFile(array(
'languages' => array($language),
'sourcePath' => $this->sourcePath,
'messagePath' => $this->messagePath,
));
$this->runMessageControllerAction('generate', array($this->configFileName));
$messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php';
// check file not overwritten:
$messageFileContent = file_get_contents($messageFileName);
$messageFileContent .= '// some not generated by command content';
file_put_contents($messageFileName, $messageFileContent);
$this->runMessageControllerAction('generate', array($this->configFileName));
$this->assertEquals($messageFileContent, file_get_contents($messageFileName));
}
/**
* @depends testCreateTranslation
*/
public function testMerge()
{
$language = 'en';
$category = 'test_category';
$messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php';
$existingMessage = 'test existing message';
$existingMessageContent = 'test existing message content';
$this->createMessageFile($messageFileName, array(
$existingMessage => $existingMessageContent
));
$newMessage = 'test new message';
$sourceFileContent = "Yii::t('{$category}', '{$existingMessage}')";
$sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')";
$this->createSourceFile($sourceFileContent);
$this->composeConfigFile(array(
'languages' => array($language),
'sourcePath' => $this->sourcePath,
'messagePath' => $this->messagePath,
'overwrite' => true,
));
$this->runMessageControllerAction('generate', array($this->configFileName));
$messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName);
$this->assertTrue(array_key_exists($newMessage, $messages), 'Unable to add new message!');
$this->assertTrue(array_key_exists($existingMessage, $messages), 'Unable to keep existing message!');
$this->assertEquals('', $messages[$newMessage], 'Wrong new message content!');
$this->assertEquals($existingMessageContent, $messages[$existingMessage], 'Unable to keep existing message content!');
}
/**
* @depends testMerge
*/
public function testNoLongerNeedTranslation()
{
$language = 'en';
$category = 'test_category';
$messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php';
$oldMessage = 'test old message';
$oldMessageContent = 'test old message content';
$this->createMessageFile($messageFileName, array(
$oldMessage => $oldMessageContent
));
$sourceFileContent = "Yii::t('{$category}', 'some new message')";
$this->createSourceFile($sourceFileContent);
$this->composeConfigFile(array(
'languages' => array($language),
'sourcePath' => $this->sourcePath,
'messagePath' => $this->messagePath,
'overwrite' => true,
'removeOld' => false,
));
$this->runMessageControllerAction('generate', array($this->configFileName));
$messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName);
$this->assertTrue(array_key_exists($oldMessage, $messages), 'No longer needed message removed!');
$this->assertEquals('@@' . $oldMessageContent . '@@', $messages[$oldMessage], 'No longer needed message content does not marked properly!');
}
/**
* @depends testMerge
*/
public function testMergeWithContentZero()
{
$language = 'en';
$category = 'test_category';
$messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php';
$zeroMessage = 'test zero message';
$zeroMessageContent = '0';
$falseMessage = 'test false message';
$falseMessageContent = 'false';
$this->createMessageFile($messageFileName, array(
$zeroMessage => $zeroMessageContent,
$falseMessage => $falseMessageContent,
));
$newMessage = 'test new message';
$sourceFileContent = "Yii::t('{$category}', '{$zeroMessage}')";
$sourceFileContent .= "Yii::t('{$category}', '{$falseMessage}')";
$sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')";
$this->createSourceFile($sourceFileContent);
$this->composeConfigFile(array(
'languages' => array($language),
'sourcePath' => $this->sourcePath,
'messagePath' => $this->messagePath,
'overwrite' => true,
));
$this->runMessageControllerAction('generate', array($this->configFileName));
$messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName);
$this->assertTrue($zeroMessageContent === $messages[$zeroMessage], 'Message content "0" is lost!');
$this->assertTrue($falseMessageContent === $messages[$falseMessage], 'Message content "false" is lost!');
}
/**
* @depends testCreateTranslation
*/
public function testMultiplyTranslators()
{
$language = 'en';
$category = 'test_category';
$translators = array(
'Yii::t',
'Custom::translate',
);
$sourceMessages = array(
'first message',
'second message',
);
$sourceFileContent = '';
foreach ($sourceMessages as $key => $message) {
$sourceFileContent .= $translators[$key] . "('{$category}', '{$message}');\n";
}
$this->createSourceFile($sourceFileContent);
$this->composeConfigFile(array(
'languages' => array($language),
'sourcePath' => $this->sourcePath,
'messagePath' => $this->messagePath,
'translator' => $translators,
));
$this->runMessageControllerAction('generate', array($this->configFileName));
$messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php';
$messages = require($messageFileName);
foreach ($sourceMessages as $sourceMessage) {
$this->assertTrue(array_key_exists($sourceMessage, $messages));
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment