Commit bc3200ce by Alexander Makarov

Fixes #4295: reworked message extraction for PO files

parent 21910c61
...@@ -12,6 +12,7 @@ use yii\console\Controller; ...@@ -12,6 +12,7 @@ use yii\console\Controller;
use yii\console\Exception; use yii\console\Exception;
use yii\helpers\FileHelper; use yii\helpers\FileHelper;
use yii\helpers\VarDumper; use yii\helpers\VarDumper;
use yii\i18n\GettextPoFile;
/** /**
* Extracts messages to be translated from source files. * Extracts messages to be translated from source files.
...@@ -88,14 +89,16 @@ class MessageController extends Controller ...@@ -88,14 +89,16 @@ class MessageController extends Controller
'format' => 'php', 'format' => 'php',
], require($configFile)); ], require($configFile));
if (!isset($config['sourcePath'], $config['messagePath'], $config['languages'])) { if (!isset($config['sourcePath'], $config['languages'])) {
throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".'); throw new Exception('The configuration file must specify "sourcePath" and "languages".');
} }
if (!is_dir($config['sourcePath'])) { if (!is_dir($config['sourcePath'])) {
throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
} }
if (in_array($config['format'], ['php', 'po'])) { if (in_array($config['format'], ['php', 'po'])) {
if (!is_dir($config['messagePath'])) { if (!isset($config['messagePath'])) {
throw new Exception('The configuration file must specify "messagePath".');
} elseif (!is_dir($config['messagePath'])) {
throw new Exception("The message path {$config['messagePath']} is not a valid directory."); throw new Exception("The message path {$config['messagePath']} is not a valid directory.");
} }
} }
...@@ -118,14 +121,11 @@ class MessageController extends Controller ...@@ -118,14 +121,11 @@ class MessageController extends Controller
if (!is_dir($dir)) { if (!is_dir($dir)) {
@mkdir($dir); @mkdir($dir);
} }
foreach ($messages as $category => $msgs) { if ($config['format'] === 'po') {
$file = str_replace("\\", '/', "$dir/$category." . $config['format']); $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages';
$path = dirname($file); $this->saveMessagesToPO($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $catalog);
if (!is_dir($path)) { } else {
mkdir($path, 0755, true); $this->saveMessagesToPHP($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort']);
}
$msgs = array_values(array_unique($msgs));
$this->generateMessageFile($msgs, $file, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['format']);
} }
} }
} elseif ($config['format'] === 'db') { } elseif ($config['format'] === 'db') {
...@@ -200,11 +200,11 @@ class MessageController extends Controller ...@@ -200,11 +200,11 @@ class MessageController extends Controller
$savedFlag = true; $savedFlag = true;
$db->createCommand() $db->createCommand()
->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute(); ->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute();
$lastId = $db->getLastInsertID(); $lastId = $db->getLastInsertID();
foreach ($languages as $language) { foreach ($languages as $language) {
$db->createCommand() $db->createCommand()
->insert($messageTable, ['id' => $lastId, 'language' => $language])->execute(); ->insert($messageTable, ['id' => $lastId, 'language' => $language])->execute();
} }
} }
} }
...@@ -217,19 +217,19 @@ class MessageController extends Controller ...@@ -217,19 +217,19 @@ class MessageController extends Controller
} else { } else {
if ($removeUnused) { if ($removeUnused) {
$db->createCommand() $db->createCommand()
->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute(); ->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute();
echo "deleted.\n"; echo "deleted.\n";
} else { } else {
$last_id = $db->getLastInsertID(); $last_id = $db->getLastInsertID();
$db->createCommand() $db->createCommand()
->update( ->update(
$sourceMessageTable, $sourceMessageTable,
['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")], ['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")],
['in', 'id', $obsolete] ['in', 'id', $obsolete]
)->execute(); )->execute();
foreach ($languages as $language) { foreach ($languages as $language) {
$db->createCommand() $db->createCommand()
->insert($messageTable, ['id' => $last_id, 'language' => $language])->execute(); ->insert($messageTable, ['id' => $last_id, 'language' => $language])->execute();
} }
echo "updated.\n"; echo "updated.\n";
} }
...@@ -273,27 +273,41 @@ class MessageController extends Controller ...@@ -273,27 +273,41 @@ class MessageController extends Controller
} }
/** /**
* Writes messages into file * Writes messages into PHP files
*
* @param array $messages
* @param string $dirName name of the directory to write to
* @param boolean $overwrite if existing file should be overwritten without backup
* @param boolean $removeUnused if obsolete translations should be removed
* @param boolean $sort if translations should be sorted
*/
protected function saveMessagesToPHP($messages, $dirName, $overwrite, $removeUnused, $sort)
{
foreach ($messages as $category => $msgs) {
$file = str_replace("\\", '/', "$dirName/$category.php");
$path = dirname($file);
if (!is_dir($path)) {
mkdir($path, 0755, true);
}
$msgs = array_values(array_unique($msgs));
echo "Saving messages to $file...\n";
$this->saveMessagesCategoryToPHP($msgs, $file, $overwrite, $removeUnused, $sort);
}
}
/**
* Writes category messages into PHP file
* *
* @param array $messages * @param array $messages
* @param string $fileName name of the file to write to * @param string $fileName name of the file to write to
* @param boolean $overwrite if existing file should be overwritten without backup * @param boolean $overwrite if existing file should be overwritten without backup
* @param boolean $removeUnused if obsolete translations should be removed * @param boolean $removeUnused if obsolete translations should be removed
* @param boolean $sort if translations should be sorted * @param boolean $sort if translations should be sorted
* @param string $format output format
*/ */
protected function generateMessageFile($messages, $fileName, $overwrite, $removeUnused, $sort, $format) protected function saveMessagesCategoryToPHP($messages, $fileName, $overwrite, $removeUnused, $sort)
{ {
echo "Saving messages to $fileName...";
if (is_file($fileName)) { if (is_file($fileName)) {
if ($format === 'po') { $translated = require($fileName);
$translated = file_get_contents($fileName);
preg_match_all('/(?<=msgid ").*(?="\n(#*)msgstr)/', $translated, $keys);
preg_match_all('/(?<=msgstr ").*(?="\n\n)/', $translated, $values);
$translated = array_combine($keys[0], $values[0]);
} else {
$translated = require($fileName);
}
sort($messages); sort($messages);
ksort($translated); ksort($translated);
if (array_keys($translated) == $messages) { if (array_keys($translated) == $messages) {
...@@ -304,9 +318,6 @@ class MessageController extends Controller ...@@ -304,9 +318,6 @@ class MessageController extends Controller
$merged = []; $merged = [];
$untranslated = []; $untranslated = [];
foreach ($messages as $message) { foreach ($messages as $message) {
if ($format === 'po') {
$message = preg_replace('/\"/', '\"', $message);
}
if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) { if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) {
$merged[$message] = $translated[$message]; $merged[$message] = $translated[$message];
} else { } else {
...@@ -336,47 +347,19 @@ class MessageController extends Controller ...@@ -336,47 +347,19 @@ class MessageController extends Controller
if (false === $overwrite) { if (false === $overwrite) {
$fileName .= '.merged'; $fileName .= '.merged';
} }
if ($format === 'po') { echo "Translation merged.\n";
$output = '';
foreach ($merged as $k => $v) {
$k = preg_replace('/(\")|(\\\")/', "\\\"", $k);
$v = preg_replace('/(\")|(\\\")/', "\\\"", $v);
if (substr($v, 0, 2) === '@@' && substr($v, -2) === '@@') {
$output .= "#msgid \"$k\"\n";
$output .= "#msgstr \"$v\"\n";
} else {
$output .= "msgid \"$k\"\n";
$output .= "msgstr \"$v\"\n";
}
$output .= "\n";
}
$merged = $output;
}
echo "translation merged.\n";
} else { } else {
if ($format === 'po') { $merged = [];
$merged = ''; foreach ($messages as $message) {
sort($messages); $merged[$message] = '';
foreach ($messages as $message) {
$message = preg_replace('/(\")|(\\\")/', '\\\"', $message);
$merged .= "msgid \"$message\"\n";
$merged .= "msgstr \"\"\n";
$merged .= "\n";
}
} else {
$merged = [];
foreach ($messages as $message) {
$merged[$message] = '';
}
ksort($merged);
} }
echo "saved.\n"; ksort($merged);
} }
if ($format === 'po') { echo "Saved.\n";
$content = $merged;
} else {
$array = VarDumper::export($merged); $array = VarDumper::export($merged);
$content = <<<EOD $content = <<<EOD
<?php <?php
/** /**
* Message translations. * Message translations.
...@@ -398,7 +381,95 @@ class MessageController extends Controller ...@@ -398,7 +381,95 @@ class MessageController extends Controller
return $array; return $array;
EOD; EOD;
}
file_put_contents($fileName, $content); file_put_contents($fileName, $content);
} }
/**
* Writes messages into PO file
*
* @param array $messages
* @param string $dirName name of the directory to write to
* @param boolean $overwrite if existing file should be overwritten without backup
* @param boolean $removeUnused if obsolete translations should be removed
* @param boolean $sort if translations should be sorted
* @param string $catalog message catalog
*/
protected function saveMessagesToPO($messages, $dirName, $overwrite, $removeUnused, $sort, $catalog)
{
$file = str_replace("\\", '/', "$dirName/$catalog.po");
$path = dirname($file);
if (!is_dir($path)) {
mkdir($path, 0755, true);
}
echo "Saving messages to $file...\n";
$poFile = new GettextPoFile();
$merged = [];
$notTranslatedYet = [];
$todos = [];
foreach ($messages as $category => $msgs) {
$msgs = array_values(array_unique($msgs));
if (is_file($file)) {
$existingMessages = $poFile->load($file, $category);
sort($msgs);
ksort($existingMessages);
if (array_keys($existingMessages) == $msgs) {
echo "Nothing new... skipped.\n";
return self::EXIT_CODE_NORMAL;
}
// merge existing message translations with new message translations
foreach ($msgs as $message) {
if (array_key_exists($message, $existingMessages) && strlen($existingMessages[$message]) > 0) {
$merged[$category . chr(4) . $message] = $existingMessages[$message];
} else {
$notTranslatedYet[] = $message;
}
}
ksort($merged);
sort($notTranslatedYet);
// collect not yet translated messages
foreach ($notTranslatedYet as $message) {
$todos[$category . chr(4) . $message] = '';
}
// add obsolete unused messages
foreach ($existingMessages as $message => $translation) {
if (!isset($merged[$category . chr(4) . $message]) && !isset($todos[$category . chr(4) . $message]) && !$removeUnused) {
if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') {
$todos[$category . chr(4) . $message] = $translation;
} else {
$todos[$category . chr(4) . $message] = '@@' . $translation . '@@';
}
}
}
$merged = array_merge($todos, $merged);
if ($sort) {
ksort($merged);
}
if ($overwrite === false) {
$file .= '.merged';
}
echo "Translation merged.\n";
} else {
sort($msgs);
foreach ($msgs as $message) {
$merged[$category . chr(4) . $message] = '';
}
ksort($merged);
}
}
$poFile->save($file, $merged);
echo "Saved.\n";
}
} }
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
return [ return [
// string, required, root directory of all source files // string, required, root directory of all source files
'sourcePath' => __DIR__, 'sourcePath' => __DIR__,
// string, required, root directory containing message translations.
'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
// array, required, list of language codes that the extracted messages // array, required, list of language codes that the extracted messages
// should be translated to. For example, ['zh-CN', 'de']. // should be translated to. For example, ['zh-CN', 'de'].
'languages' => ['de'], 'languages' => ['de'],
...@@ -44,9 +42,30 @@ return [ ...@@ -44,9 +42,30 @@ return [
'.hgkeep', '.hgkeep',
'/messages', '/messages',
], ],
// Generated file format. Can be either "php", "po" or "db".
// 'php' output format is for saving messages to php files.
'format' => 'php', 'format' => 'php',
// When format is "db", you may specify the following two options // Root directory containing message translations.
//'db' => 'db', 'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
//'sourceMessageTable' => '{{%source_message}}',
/*
// 'db' output format is for saving messages to database.
'format' => 'db',
// Connection component to use. Optional.
'db' => 'db',
// Custom source message table. Optional.
// 'sourceMessageTable' => '{{%source_message}}',
// Custom name for translation message table. Optional.
// 'messageTable' => '{{%message}}',
*/
/*
// 'po' output format is for saving messages to gettext po files.
'format' => 'po',
// Root directory containing message translations.
'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
// Name of the file that will be used for translations.
'catalog' => 'messages',
*/
]; ];
...@@ -255,13 +255,13 @@ class MessageControllerTest extends TestCase ...@@ -255,13 +255,13 @@ class MessageControllerTest extends TestCase
'messagePath' => $this->messagePath, 'messagePath' => $this->messagePath,
'overwrite' => true, 'overwrite' => true,
]); ]);
$this->runMessageControllerAction('extract', [$this->configFileName]); $commandOutput = $this->runMessageControllerAction('extract', [$this->configFileName]);
$messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName);
$this->assertTrue(array_key_exists($newMessage, $messages), 'Unable to add new message!'); $this->assertTrue(array_key_exists($newMessage, $messages), 'Unable to add new message: "' . $newMessage . '". Command output was:' . "\n" . $commandOutput);
$this->assertTrue(array_key_exists($existingMessage, $messages), 'Unable to keep existing message!'); $this->assertTrue(array_key_exists($existingMessage, $messages), 'Unable to keep existing message: "' . $existingMessage . '". Command output was:' . "\n" . $commandOutput);
$this->assertEquals('', $messages[$newMessage], 'Wrong new message content!'); $this->assertEquals('', $messages[$newMessage], 'Wrong new message content!. Command output was:\n' . $commandOutput);
$this->assertEquals($existingMessageContent, $messages[$existingMessage], 'Unable to keep existing message content!'); $this->assertEquals($existingMessageContent, $messages[$existingMessage], 'Unable to keep existing message content!. Command output was:' . "\n" . $commandOutput);
} }
/** /**
......
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