MessageController.php 9.12 KB
Newer Older
Qiang Xue committed
1 2 3 4
<?php
/**
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
Qiang Xue committed
5
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
6 7 8
 * @license http://www.yiiframework.com/license/
 */

9 10 11
namespace yii\console\controllers;

use yii\console\Controller;
12 13
use yii\console\Exception;
use yii\helpers\FileHelper;
14

Qiang Xue committed
15
/**
16
 * This command extracts messages to be translated from source files.
Qiang Xue committed
17 18 19
 * The extracted messages are saved as PHP message source files
 * under the specified directory.
 *
20 21 22 23 24 25 26
 * 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
 *
Qiang Xue committed
27
 * @author Qiang Xue <qiang.xue@gmail.com>
28
 * @since 2.0
Qiang Xue committed
29
 */
30
class MessageController extends Controller
Qiang Xue committed
31
{
32 33 34 35
	/**
	 * @var string controller default action ID.
	 */
	public $defaultAction = 'generate';
Qiang Xue committed
36
	/**
37 38 39 40 41
	 * Searches for messages to be translated in the specified
	 * source files and compiles them into PHP arrays as message source.
	 *
	 * @param string $config the path of the configuration file. You can find
	 * an example in framework/messages/config.php.
42
	 * @throws \yii\console\Exception on failure.
43 44 45 46 47 48 49 50 51 52
	 *
	 * 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
	 * a configuration option.
	 *
	 * The following options are available:
	 *
	 *  - sourcePath: string, root directory of all source files.
	 *  - messagePath: string, root directory containing message translations.
	 *  - languages: array, list of language codes that the extracted messages
resurtm committed
53
	 *    should be translated to. For example, array('zh_cn', 'en_au').
54 55 56 57 58 59 60 61 62 63 64
	 *  - fileTypes: array, a list of file extensions (e.g. 'php', 'xml').
	 *    Only the files whose extension name can be found in this list
	 *    will be processed. If empty, all files will be processed.
	 *  - exclude: array, a list of directory and file exclusions. Each
	 *    exclusion can be either a name or a path. If a file or directory name
	 *    or path matches the exclusion, it will not be copied. For example,
	 *    an exclusion of '.svn' will exclude all files and directories whose
	 *    name is '.svn'. And an exclusion of '/a/b' will exclude file or
	 *    directory 'sourcePath/a/b'.
	 *  - translator: the name of the function for translating messages.
	 *    Defaults to 'Yii::t'. This is used as a mark to find messages to be
65 66
	 *    translated. Accepts both string for single function name or array for
	 *    multiple function names.
67 68 69 70 71
	 *  - overwrite: if message file must be overwritten with the merged messages.
	 *  - removeOld: if message no longer needs translation it will be removed,
	 *    instead of being enclosed between a pair of '@@' marks.
	 *  - sort: sort messages by key when merging, regardless of their translation
	 *    state (new, obsolete, translated.)
Qiang Xue committed
72
	 */
73
	public function actionGenerate($config)
Qiang Xue committed
74
	{
resurtm committed
75
		if (!is_file($config)) {
76
			throw new Exception("the configuration file {$config} does not exist.");
resurtm committed
77
		}
78

79
		$config = require($config);
Qiang Xue committed
80

81
		$translator = 'Yii::t';
Qiang Xue committed
82 83
		extract($config);

resurtm committed
84
		if (!isset($sourcePath, $messagePath, $languages)) {
85
			throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".');
resurtm committed
86 87
		}
		if (!is_dir($sourcePath)) {
88
			throw new Exception("The source path {$sourcePath} is not a valid directory.");
resurtm committed
89 90
		}
		if (!is_dir($messagePath)) {
91
			throw new Exception("The message path {$messagePath} is not a valid directory.");
resurtm committed
92 93
		}
		if (empty($languages)) {
94
			throw new Exception("Languages cannot be empty.");
resurtm committed
95
		}
Qiang Xue committed
96

resurtm committed
97
		if (!isset($overwrite)) {
Qiang Xue committed
98
			$overwrite = false;
resurtm committed
99 100
		}
		if (!isset($removeOld)) {
Qiang Xue committed
101
			$removeOld = false;
resurtm committed
102 103
		}
		if (!isset($sort)) {
Qiang Xue committed
104
			$sort = false;
resurtm committed
105
		}
106

resurtm committed
107 108 109 110 111 112 113
		$options = array();
		if (isset($fileTypes)) {
			$options['fileTypes'] = $fileTypes;
		}
		if (isset($exclude)) {
			$options['exclude'] = $exclude;
		}
114
		$files = FileHelper::findFiles(realpath($sourcePath), $options);
Qiang Xue committed
115

resurtm committed
116 117 118 119
		$messages = array();
		foreach ($files as $file) {
			$messages = array_merge_recursive($messages, $this->extractMessages($file, $translator));
		}
Qiang Xue committed
120

resurtm committed
121 122 123
		foreach ($languages as $language) {
			$dir = $messagePath . DIRECTORY_SEPARATOR . $language;
			if (!is_dir($dir)) {
Qiang Xue committed
124
				@mkdir($dir);
resurtm committed
125 126 127 128
			}
			foreach ($messages as $category => $msgs) {
				$msgs = array_values(array_unique($msgs));
				$this->generateMessageFile($msgs, $dir . DIRECTORY_SEPARATOR . $category . '.php', $overwrite, $removeOld, $sort);
Qiang Xue committed
129 130 131 132
			}
		}
	}

Alexander Makarov committed
133 134 135 136 137 138 139
	/**
	 * Extracts messages from a file
	 *
	 * @param string $fileName name of the file to extract messages from
	 * @param string $translator name of the function used to translate messages
	 * @return array
	 */
resurtm committed
140
	protected function extractMessages($fileName, $translator)
Qiang Xue committed
141 142
	{
		echo "Extracting messages from $fileName...\n";
resurtm committed
143 144
		$subject = file_get_contents($fileName);
		$messages = array();
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
		if (!is_array($translator)) {
			$translator = array($translator);
		}
		foreach ($translator as $currentTranslator) {
			$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
resurtm committed
160
			}
Qiang Xue committed
161 162 163 164
		}
		return $messages;
	}

Alexander Makarov committed
165 166 167 168 169 170 171 172 173
	/**
	 * Writes messages into file
	 *
	 * @param array $messages
	 * @param string $fileName name of the file to write to
	 * @param boolean $overwrite if existing file should be overwritten without backup
	 * @param boolean $removeOld if obsolete translations should be removed
	 * @param boolean $sort if translations should be sorted
	 */
resurtm committed
174
	protected function generateMessageFile($messages, $fileName, $overwrite, $removeOld, $sort)
Qiang Xue committed
175 176
	{
		echo "Saving messages to $fileName...";
resurtm committed
177 178
		if (is_file($fileName)) {
			$translated = require($fileName);
Qiang Xue committed
179 180
			sort($messages);
			ksort($translated);
resurtm committed
181
			if (array_keys($translated) == $messages) {
Qiang Xue committed
182 183 184
				echo "nothing new...skipped.\n";
				return;
			}
resurtm committed
185 186 187
			$merged = array();
			$untranslated = array();
			foreach ($messages as $message) {
188
				if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) {
resurtm committed
189 190 191 192
					$merged[$message] = $translated[$message];
				} else {
					$untranslated[] = $message;
				}
Qiang Xue committed
193 194 195
			}
			ksort($merged);
			sort($untranslated);
resurtm committed
196 197 198 199
			$todo = array();
			foreach ($untranslated as $message) {
				$todo[$message] = '';
			}
Qiang Xue committed
200
			ksort($translated);
resurtm committed
201
			foreach ($translated as $message => $translation) {
resurtm committed
202
				if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeOld) {
resurtm committed
203
					if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') {
Qiang Xue committed
204
						$todo[$message]=$translation;
resurtm committed
205 206 207
					} else {
						$todo[$message] = '@@' . $translation . '@@';
					}
Qiang Xue committed
208 209
				}
			}
resurtm committed
210 211
			$merged = array_merge($todo, $merged);
			if ($sort) {
Qiang Xue committed
212
				ksort($merged);
resurtm committed
213 214 215 216
			}
			if (false === $overwrite) {
				$fileName .= '.merged';
			}
Qiang Xue committed
217
			echo "translation merged.\n";
resurtm committed
218 219 220 221 222
		} else {
			$merged = array();
			foreach ($messages as $message) {
				$merged[$message] = '';
			}
Qiang Xue committed
223 224 225
			ksort($merged);
			echo "saved.\n";
		}
resurtm committed
226 227
		$array = str_replace("\r", '', var_export($merged, true));
		$content = <<<EOD
Qiang Xue committed
228 229 230 231
<?php
/**
 * Message translations.
 *
232
 * This file is automatically generated by 'yii {$this->id}' command.
Qiang Xue committed
233 234 235 236 237 238 239 240 241 242 243
 * It contains the localizable messages extracted from source code.
 * You may modify this file by translating the extracted messages.
 *
 * Each array element represents the translation (value) of a message (key).
 * If the value is empty, the message is considered as not translated.
 * Messages that no longer need translation will have their translations
 * enclosed between a pair of '@@' marks.
 *
 * Message string can be used with plural forms format. Check i18n section
 * of the guide for details.
 *
244
 * NOTE: this file must be saved in UTF-8 encoding.
Qiang Xue committed
245 246 247 248 249 250
 */
return $array;

EOD;
		file_put_contents($fileName, $content);
	}
251 252

	/**
253
	 * Creates template of configuration file for [[actionGenerate]].
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
	 * @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";
		}
	}
Qiang Xue committed
291
}