pictcode / lib / Cake / Console / Command / Task / ExtractTask.php @ fbcf274f
履歴 | 表示 | アノテート | ダウンロード (24.812 KB)
| 1 |
<?php
|
|---|---|
| 2 |
/**
|
| 3 |
* Language string extractor
|
| 4 |
*
|
| 5 |
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
| 6 |
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
| 7 |
*
|
| 8 |
* Licensed under The MIT License
|
| 9 |
* For full copyright and license information, please see the LICENSE.txt
|
| 10 |
* Redistributions of files must retain the above copyright notice.
|
| 11 |
*
|
| 12 |
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
| 13 |
* @link http://cakephp.org CakePHP(tm) Project
|
| 14 |
* @since CakePHP(tm) v 1.2.0.5012
|
| 15 |
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
| 16 |
*/
|
| 17 |
|
| 18 |
App::uses('AppShell', 'Console/Command'); |
| 19 |
App::uses('File', 'Utility'); |
| 20 |
App::uses('Folder', 'Utility'); |
| 21 |
App::uses('Hash', 'Utility'); |
| 22 |
|
| 23 |
/**
|
| 24 |
* Language string extractor
|
| 25 |
*
|
| 26 |
* @package Cake.Console.Command.Task
|
| 27 |
*/
|
| 28 |
class ExtractTask extends AppShell { |
| 29 |
|
| 30 |
/**
|
| 31 |
* Paths to use when looking for strings
|
| 32 |
*
|
| 33 |
* @var string
|
| 34 |
*/
|
| 35 |
protected $_paths = array(); |
| 36 |
|
| 37 |
/**
|
| 38 |
* Files from where to extract
|
| 39 |
*
|
| 40 |
* @var array
|
| 41 |
*/
|
| 42 |
protected $_files = array(); |
| 43 |
|
| 44 |
/**
|
| 45 |
* Merge all domain and category strings into the default.pot file
|
| 46 |
*
|
| 47 |
* @var bool
|
| 48 |
*/
|
| 49 |
protected $_merge = false; |
| 50 |
|
| 51 |
/**
|
| 52 |
* Current file being processed
|
| 53 |
*
|
| 54 |
* @var string
|
| 55 |
*/
|
| 56 |
protected $_file = null; |
| 57 |
|
| 58 |
/**
|
| 59 |
* Contains all content waiting to be write
|
| 60 |
*
|
| 61 |
* @var string
|
| 62 |
*/
|
| 63 |
protected $_storage = array(); |
| 64 |
|
| 65 |
/**
|
| 66 |
* Extracted tokens
|
| 67 |
*
|
| 68 |
* @var array
|
| 69 |
*/
|
| 70 |
protected $_tokens = array(); |
| 71 |
|
| 72 |
/**
|
| 73 |
* Extracted strings indexed by category, domain, msgid and context.
|
| 74 |
*
|
| 75 |
* @var array
|
| 76 |
*/
|
| 77 |
protected $_translations = array(); |
| 78 |
|
| 79 |
/**
|
| 80 |
* Destination path
|
| 81 |
*
|
| 82 |
* @var string
|
| 83 |
*/
|
| 84 |
protected $_output = null; |
| 85 |
|
| 86 |
/**
|
| 87 |
* An array of directories to exclude.
|
| 88 |
*
|
| 89 |
* @var array
|
| 90 |
*/
|
| 91 |
protected $_exclude = array(); |
| 92 |
|
| 93 |
/**
|
| 94 |
* Holds whether this call should extract model validation messages
|
| 95 |
*
|
| 96 |
* @var bool
|
| 97 |
*/
|
| 98 |
protected $_extractValidation = true; |
| 99 |
|
| 100 |
/**
|
| 101 |
* Holds the validation string domain to use for validation messages when extracting
|
| 102 |
*
|
| 103 |
* @var bool
|
| 104 |
*/
|
| 105 |
protected $_validationDomain = 'default'; |
| 106 |
|
| 107 |
/**
|
| 108 |
* Holds whether this call should extract the CakePHP Lib messages
|
| 109 |
*
|
| 110 |
* @var bool
|
| 111 |
*/
|
| 112 |
protected $_extractCore = false; |
| 113 |
|
| 114 |
/**
|
| 115 |
* Method to interact with the User and get path selections.
|
| 116 |
*
|
| 117 |
* @return void
|
| 118 |
*/
|
| 119 |
protected function _getPaths() { |
| 120 |
$defaultPath = APP; |
| 121 |
while (true) { |
| 122 |
$currentPaths = count($this->_paths) > 0 ? $this->_paths : array('None'); |
| 123 |
$message = __d(
|
| 124 |
'cake_console',
|
| 125 |
"Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one",
|
| 126 |
implode(', ', $currentPaths) |
| 127 |
); |
| 128 |
$response = $this->in($message, null, $defaultPath); |
| 129 |
if (strtoupper($response) === 'Q') { |
| 130 |
$this->err(__d('cake_console', 'Extract Aborted')); |
| 131 |
return $this->_stop(); |
| 132 |
} elseif (strtoupper($response) === 'D' && count($this->_paths)) { |
| 133 |
$this->out();
|
| 134 |
return;
|
| 135 |
} elseif (strtoupper($response) === 'D') { |
| 136 |
$this->err(__d('cake_console', '<warning>No directories selected.</warning> Please choose a directory.')); |
| 137 |
} elseif (is_dir($response)) { |
| 138 |
$this->_paths[] = $response; |
| 139 |
$defaultPath = 'D'; |
| 140 |
} else {
|
| 141 |
$this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.')); |
| 142 |
} |
| 143 |
$this->out();
|
| 144 |
} |
| 145 |
} |
| 146 |
|
| 147 |
/**
|
| 148 |
* Execution method always used for tasks
|
| 149 |
*
|
| 150 |
* @return void
|
| 151 |
*/
|
| 152 |
public function execute() { |
| 153 |
if (!empty($this->params['exclude'])) { |
| 154 |
$this->_exclude = explode(',', $this->params['exclude']); |
| 155 |
} |
| 156 |
if (isset($this->params['files']) && !is_array($this->params['files'])) { |
| 157 |
$this->_files = explode(',', $this->params['files']); |
| 158 |
} |
| 159 |
if (isset($this->params['paths'])) { |
| 160 |
$this->_paths = explode(',', $this->params['paths']); |
| 161 |
} elseif (isset($this->params['plugin'])) { |
| 162 |
$plugin = Inflector::camelize($this->params['plugin']); |
| 163 |
if (!CakePlugin::loaded($plugin)) { |
| 164 |
CakePlugin::load($plugin); |
| 165 |
} |
| 166 |
$this->_paths = array(CakePlugin::path($plugin)); |
| 167 |
$this->params['plugin'] = $plugin; |
| 168 |
} else {
|
| 169 |
$this->_getPaths();
|
| 170 |
} |
| 171 |
|
| 172 |
if (isset($this->params['extract-core'])) { |
| 173 |
$this->_extractCore = !(strtolower($this->params['extract-core']) === 'no'); |
| 174 |
} else {
|
| 175 |
$response = $this->in(__d('cake_console', 'Would you like to extract the messages from the CakePHP core?'), array('y', 'n'), 'n'); |
| 176 |
$this->_extractCore = strtolower($response) === 'y'; |
| 177 |
} |
| 178 |
|
| 179 |
if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) { |
| 180 |
$this->_exclude = array_merge($this->_exclude, App::path('plugins')); |
| 181 |
} |
| 182 |
|
| 183 |
if (!empty($this->params['ignore-model-validation']) || (!$this->_isExtractingApp() && empty($plugin))) { |
| 184 |
$this->_extractValidation = false; |
| 185 |
} |
| 186 |
if (!empty($this->params['validation-domain'])) { |
| 187 |
$this->_validationDomain = $this->params['validation-domain']; |
| 188 |
} |
| 189 |
|
| 190 |
if ($this->_extractCore) { |
| 191 |
$this->_paths[] = CAKE; |
| 192 |
$this->_exclude = array_merge($this->_exclude, array( |
| 193 |
CAKE . 'Test', |
| 194 |
CAKE . 'Console' . DS . 'Templates' |
| 195 |
)); |
| 196 |
} |
| 197 |
|
| 198 |
if (isset($this->params['output'])) { |
| 199 |
$this->_output = $this->params['output']; |
| 200 |
} elseif (isset($this->params['plugin'])) { |
| 201 |
$this->_output = $this->_paths[0] . DS . 'Locale'; |
| 202 |
} else {
|
| 203 |
$message = __d('cake_console', "What is the path you would like to output?\n[Q]uit", $this->_paths[0] . DS . 'Locale'); |
| 204 |
while (true) { |
| 205 |
$response = $this->in($message, null, rtrim($this->_paths[0], DS) . DS . 'Locale'); |
| 206 |
if (strtoupper($response) === 'Q') { |
| 207 |
$this->err(__d('cake_console', 'Extract Aborted')); |
| 208 |
return $this->_stop(); |
| 209 |
} elseif ($this->_isPathUsable($response)) { |
| 210 |
$this->_output = $response . DS; |
| 211 |
break;
|
| 212 |
} else {
|
| 213 |
$this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.')); |
| 214 |
} |
| 215 |
$this->out();
|
| 216 |
} |
| 217 |
} |
| 218 |
|
| 219 |
if (isset($this->params['merge'])) { |
| 220 |
$this->_merge = !(strtolower($this->params['merge']) === 'no'); |
| 221 |
} else {
|
| 222 |
$this->out();
|
| 223 |
$response = $this->in(__d('cake_console', 'Would you like to merge all domain and category strings into the default.pot file?'), array('y', 'n'), 'n'); |
| 224 |
$this->_merge = strtolower($response) === 'y'; |
| 225 |
} |
| 226 |
|
| 227 |
if (empty($this->_files)) { |
| 228 |
$this->_searchFiles();
|
| 229 |
} |
| 230 |
|
| 231 |
$this->_output = rtrim($this->_output, DS) . DS; |
| 232 |
if (!$this->_isPathUsable($this->_output)) { |
| 233 |
$this->err(__d('cake_console', 'The output directory %s was not found or writable.', $this->_output)); |
| 234 |
return $this->_stop(); |
| 235 |
} |
| 236 |
|
| 237 |
$this->_extract();
|
| 238 |
} |
| 239 |
|
| 240 |
/**
|
| 241 |
* Add a translation to the internal translations property
|
| 242 |
*
|
| 243 |
* Takes care of duplicate translations
|
| 244 |
*
|
| 245 |
* @param string $category The category
|
| 246 |
* @param string $domain The domain
|
| 247 |
* @param string $msgid The message string
|
| 248 |
* @param array $details The file and line references
|
| 249 |
* @return void
|
| 250 |
*/
|
| 251 |
protected function _addTranslation($category, $domain, $msgid, $details = array()) { |
| 252 |
$context = ''; |
| 253 |
if (isset($details['msgctxt'])) { |
| 254 |
$context = $details['msgctxt']; |
| 255 |
} |
| 256 |
|
| 257 |
if (empty($this->_translations[$category][$domain][$msgid][$context])) { |
| 258 |
$this->_translations[$category][$domain][$msgid][$context] = array( |
| 259 |
'msgid_plural' => false, |
| 260 |
); |
| 261 |
} |
| 262 |
|
| 263 |
if (isset($details['msgid_plural'])) { |
| 264 |
$this->_translations[$category][$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural']; |
| 265 |
} |
| 266 |
if (isset($details['file'])) { |
| 267 |
$line = 0; |
| 268 |
if (isset($details['line'])) { |
| 269 |
$line = $details['line']; |
| 270 |
} |
| 271 |
$this->_translations[$category][$domain][$msgid][$context]['references'][$details['file']][] = $line; |
| 272 |
} |
| 273 |
} |
| 274 |
|
| 275 |
/**
|
| 276 |
* Extract text
|
| 277 |
*
|
| 278 |
* @return void
|
| 279 |
*/
|
| 280 |
protected function _extract() { |
| 281 |
$this->out();
|
| 282 |
$this->out();
|
| 283 |
$this->out(__d('cake_console', 'Extracting...')); |
| 284 |
$this->hr();
|
| 285 |
$this->out(__d('cake_console', 'Paths:')); |
| 286 |
foreach ($this->_paths as $path) { |
| 287 |
$this->out(' ' . $path); |
| 288 |
} |
| 289 |
$this->out(__d('cake_console', 'Output Directory: ') . $this->_output); |
| 290 |
$this->hr();
|
| 291 |
$this->_extractTokens();
|
| 292 |
$this->_extractValidationMessages();
|
| 293 |
$this->_buildFiles();
|
| 294 |
$this->_writeFiles();
|
| 295 |
$this->_paths = $this->_files = $this->_storage = array(); |
| 296 |
$this->_translations = $this->_tokens = array(); |
| 297 |
$this->_extractValidation = true; |
| 298 |
$this->out();
|
| 299 |
$this->out(__d('cake_console', 'Done.')); |
| 300 |
} |
| 301 |
|
| 302 |
/**
|
| 303 |
* Gets the option parser instance and configures it.
|
| 304 |
*
|
| 305 |
* @return ConsoleOptionParser
|
| 306 |
*/
|
| 307 |
public function getOptionParser() { |
| 308 |
$parser = parent::getOptionParser(); |
| 309 |
|
| 310 |
$parser->description(
|
| 311 |
__d('cake_console', 'CakePHP Language String Extraction:') |
| 312 |
)->addOption('app', array( |
| 313 |
'help' => __d('cake_console', 'Directory where your application is located.') |
| 314 |
))->addOption('paths', array( |
| 315 |
'help' => __d('cake_console', 'Comma separated list of paths.') |
| 316 |
))->addOption('merge', array( |
| 317 |
'help' => __d('cake_console', 'Merge all domain and category strings into the default.po file.'), |
| 318 |
'choices' => array('yes', 'no') |
| 319 |
))->addOption('output', array( |
| 320 |
'help' => __d('cake_console', 'Full path to output directory.') |
| 321 |
))->addOption('files', array( |
| 322 |
'help' => __d('cake_console', 'Comma separated list of files.') |
| 323 |
))->addOption('exclude-plugins', array( |
| 324 |
'boolean' => true, |
| 325 |
'default' => true, |
| 326 |
'help' => __d('cake_console', 'Ignores all files in plugins if this command is run inside from the same app directory.') |
| 327 |
))->addOption('plugin', array( |
| 328 |
'help' => __d('cake_console', 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.') |
| 329 |
))->addOption('ignore-model-validation', array( |
| 330 |
'boolean' => true, |
| 331 |
'default' => false, |
| 332 |
'help' => __d('cake_console', 'Ignores validation messages in the $validate property.' . |
| 333 |
' If this flag is not set and the command is run from the same app directory,' .
|
| 334 |
' all messages in model validation rules will be extracted as tokens.'
|
| 335 |
) |
| 336 |
))->addOption('validation-domain', array( |
| 337 |
'help' => __d('cake_console', 'If set to a value, the localization domain to be used for model validation messages.') |
| 338 |
))->addOption('exclude', array( |
| 339 |
'help' => __d('cake_console', 'Comma separated list of directories to exclude.' . |
| 340 |
' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors'
|
| 341 |
) |
| 342 |
))->addOption('overwrite', array( |
| 343 |
'boolean' => true, |
| 344 |
'default' => false, |
| 345 |
'help' => __d('cake_console', 'Always overwrite existing .pot files.') |
| 346 |
))->addOption('extract-core', array( |
| 347 |
'help' => __d('cake_console', 'Extract messages from the CakePHP core libs.'), |
| 348 |
'choices' => array('yes', 'no') |
| 349 |
)); |
| 350 |
|
| 351 |
return $parser; |
| 352 |
} |
| 353 |
|
| 354 |
/**
|
| 355 |
* Extract tokens out of all files to be processed
|
| 356 |
*
|
| 357 |
* @return void
|
| 358 |
*/
|
| 359 |
protected function _extractTokens() { |
| 360 |
foreach ($this->_files as $file) { |
| 361 |
$this->_file = $file; |
| 362 |
$this->out(__d('cake_console', 'Processing %s...', $file), 1, Shell::VERBOSE); |
| 363 |
|
| 364 |
$code = file_get_contents($file); |
| 365 |
$allTokens = token_get_all($code); |
| 366 |
|
| 367 |
$this->_tokens = array(); |
| 368 |
foreach ($allTokens as $token) { |
| 369 |
if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) { |
| 370 |
$this->_tokens[] = $token; |
| 371 |
} |
| 372 |
} |
| 373 |
unset($allTokens); |
| 374 |
$this->_parse('__', array('singular')); |
| 375 |
$this->_parse('__n', array('singular', 'plural')); |
| 376 |
$this->_parse('__d', array('domain', 'singular')); |
| 377 |
$this->_parse('__c', array('singular', 'category')); |
| 378 |
$this->_parse('__dc', array('domain', 'singular', 'category')); |
| 379 |
$this->_parse('__dn', array('domain', 'singular', 'plural')); |
| 380 |
$this->_parse('__dcn', array('domain', 'singular', 'plural', 'count', 'category')); |
| 381 |
|
| 382 |
$this->_parse('__x', array('context', 'singular')); |
| 383 |
$this->_parse('__xn', array('context', 'singular', 'plural')); |
| 384 |
$this->_parse('__dx', array('domain', 'context', 'singular')); |
| 385 |
$this->_parse('__dxc', array('domain', 'context', 'singular', 'category')); |
| 386 |
$this->_parse('__dxn', array('domain', 'context', 'singular', 'plural')); |
| 387 |
$this->_parse('__dxcn', array('domain', 'context', 'singular', 'plural', 'count', 'category')); |
| 388 |
$this->_parse('__xc', array('context', 'singular', 'category')); |
| 389 |
|
| 390 |
} |
| 391 |
} |
| 392 |
|
| 393 |
/**
|
| 394 |
* Parse tokens
|
| 395 |
*
|
| 396 |
* @param string $functionName Function name that indicates translatable string (e.g: '__')
|
| 397 |
* @param array $map Array containing what variables it will find (e.g: category, domain, singular, plural)
|
| 398 |
* @return void
|
| 399 |
*/
|
| 400 |
protected function _parse($functionName, $map) { |
| 401 |
$count = 0; |
| 402 |
$categories = array('LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'); |
| 403 |
$tokenCount = count($this->_tokens); |
| 404 |
|
| 405 |
while (($tokenCount - $count) > 1) { |
| 406 |
$countToken = $this->_tokens[$count]; |
| 407 |
$firstParenthesis = $this->_tokens[$count + 1]; |
| 408 |
if (!is_array($countToken)) { |
| 409 |
$count++;
|
| 410 |
continue;
|
| 411 |
} |
| 412 |
|
| 413 |
list($type, $string, $line) = $countToken; |
| 414 |
if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) { |
| 415 |
$position = $count; |
| 416 |
$depth = 0; |
| 417 |
|
| 418 |
while (!$depth) { |
| 419 |
if ($this->_tokens[$position] === '(') { |
| 420 |
$depth++;
|
| 421 |
} elseif ($this->_tokens[$position] === ')') { |
| 422 |
$depth--;
|
| 423 |
} |
| 424 |
$position++;
|
| 425 |
} |
| 426 |
|
| 427 |
$mapCount = count($map); |
| 428 |
$strings = $this->_getStrings($position, $mapCount); |
| 429 |
|
| 430 |
if ($mapCount === count($strings)) { |
| 431 |
extract(array_combine($map, $strings)); |
| 432 |
$category = isset($category) ? $category : 6; |
| 433 |
$category = (int)$category; |
| 434 |
$categoryName = $categories[$category]; |
| 435 |
|
| 436 |
$domain = isset($domain) ? $domain : 'default'; |
| 437 |
$details = array( |
| 438 |
'file' => $this->_file, |
| 439 |
'line' => $line, |
| 440 |
); |
| 441 |
if (isset($plural)) { |
| 442 |
$details['msgid_plural'] = $plural; |
| 443 |
} |
| 444 |
if (isset($context)) { |
| 445 |
$details['msgctxt'] = $context; |
| 446 |
} |
| 447 |
// Skip LC_TIME files as we use a special file format for them.
|
| 448 |
if ($categoryName !== 'LC_TIME') { |
| 449 |
$this->_addTranslation($categoryName, $domain, $singular, $details); |
| 450 |
} |
| 451 |
} elseif (!is_array($this->_tokens[$count - 1]) || $this->_tokens[$count - 1][0] != T_FUNCTION) { |
| 452 |
$this->_markerError($this->_file, $line, $functionName, $count); |
| 453 |
} |
| 454 |
} |
| 455 |
$count++;
|
| 456 |
} |
| 457 |
} |
| 458 |
|
| 459 |
/**
|
| 460 |
* Looks for models in the application and extracts the validation messages
|
| 461 |
* to be added to the translation map
|
| 462 |
*
|
| 463 |
* @return void
|
| 464 |
*/
|
| 465 |
protected function _extractValidationMessages() { |
| 466 |
if (!$this->_extractValidation) { |
| 467 |
return;
|
| 468 |
} |
| 469 |
|
| 470 |
$plugins = array(null); |
| 471 |
if (empty($this->params['exclude-plugins'])) { |
| 472 |
$plugins = array_merge($plugins, App::objects('plugin', null, false)); |
| 473 |
} |
| 474 |
foreach ($plugins as $plugin) { |
| 475 |
$this->_extractPluginValidationMessages($plugin); |
| 476 |
} |
| 477 |
} |
| 478 |
|
| 479 |
/**
|
| 480 |
* Extract validation messages from application or plugin models
|
| 481 |
*
|
| 482 |
* @param string $plugin Plugin name or `null` to process application models
|
| 483 |
* @return void
|
| 484 |
*/
|
| 485 |
protected function _extractPluginValidationMessages($plugin = null) { |
| 486 |
App::uses('AppModel', 'Model'); |
| 487 |
if (!empty($plugin)) { |
| 488 |
if (!CakePlugin::loaded($plugin)) { |
| 489 |
return;
|
| 490 |
} |
| 491 |
App::uses($plugin . 'AppModel', $plugin . '.Model'); |
| 492 |
$plugin = $plugin . '.'; |
| 493 |
} |
| 494 |
$models = App::objects($plugin . 'Model', null, false); |
| 495 |
|
| 496 |
foreach ($models as $model) { |
| 497 |
App::uses($model, $plugin . 'Model'); |
| 498 |
$reflection = new ReflectionClass($model); |
| 499 |
if (!$reflection->isSubClassOf('Model')) { |
| 500 |
continue;
|
| 501 |
} |
| 502 |
$properties = $reflection->getDefaultProperties(); |
| 503 |
$validate = $properties['validate']; |
| 504 |
if (empty($validate)) { |
| 505 |
continue;
|
| 506 |
} |
| 507 |
|
| 508 |
$file = $reflection->getFileName(); |
| 509 |
$domain = $this->_validationDomain; |
| 510 |
if (!empty($properties['validationDomain'])) { |
| 511 |
$domain = $properties['validationDomain']; |
| 512 |
} |
| 513 |
foreach ($validate as $field => $rules) { |
| 514 |
$this->_processValidationRules($field, $rules, $file, $domain); |
| 515 |
} |
| 516 |
} |
| 517 |
} |
| 518 |
|
| 519 |
/**
|
| 520 |
* Process a validation rule for a field and looks for a message to be added
|
| 521 |
* to the translation map
|
| 522 |
*
|
| 523 |
* @param string $field the name of the field that is being processed
|
| 524 |
* @param array $rules the set of validation rules for the field
|
| 525 |
* @param string $file the file name where this validation rule was found
|
| 526 |
* @param string $domain default domain to bind the validations to
|
| 527 |
* @param string $category the translation category
|
| 528 |
* @return void
|
| 529 |
*/
|
| 530 |
protected function _processValidationRules($field, $rules, $file, $domain, $category = 'LC_MESSAGES') { |
| 531 |
if (!is_array($rules)) { |
| 532 |
return;
|
| 533 |
} |
| 534 |
|
| 535 |
$dims = Hash::dimensions($rules); |
| 536 |
if ($dims === 1 || ($dims === 2 && isset($rules['message']))) { |
| 537 |
$rules = array($rules); |
| 538 |
} |
| 539 |
|
| 540 |
foreach ($rules as $rule => $validateProp) { |
| 541 |
$msgid = null; |
| 542 |
if (isset($validateProp['message'])) { |
| 543 |
if (is_array($validateProp['message'])) { |
| 544 |
$msgid = $validateProp['message'][0]; |
| 545 |
} else {
|
| 546 |
$msgid = $validateProp['message']; |
| 547 |
} |
| 548 |
} elseif (is_string($rule)) { |
| 549 |
$msgid = $rule; |
| 550 |
} |
| 551 |
if ($msgid) { |
| 552 |
$msgid = $this->_formatString(sprintf("'%s'", $msgid)); |
| 553 |
$details = array( |
| 554 |
'file' => $file, |
| 555 |
'line' => 'validation for field ' . $field |
| 556 |
); |
| 557 |
$this->_addTranslation($category, $domain, $msgid, $details); |
| 558 |
} |
| 559 |
} |
| 560 |
} |
| 561 |
|
| 562 |
/**
|
| 563 |
* Build the translate template file contents out of obtained strings
|
| 564 |
*
|
| 565 |
* @return void
|
| 566 |
*/
|
| 567 |
protected function _buildFiles() { |
| 568 |
$paths = $this->_paths; |
| 569 |
$paths[] = realpath(APP) . DS; |
| 570 |
foreach ($this->_translations as $category => $domains) { |
| 571 |
foreach ($domains as $domain => $translations) { |
| 572 |
foreach ($translations as $msgid => $contexts) { |
| 573 |
foreach ($contexts as $context => $details) { |
| 574 |
$plural = $details['msgid_plural']; |
| 575 |
$files = $details['references']; |
| 576 |
$occurrences = array(); |
| 577 |
foreach ($files as $file => $lines) { |
| 578 |
$lines = array_unique($lines); |
| 579 |
$occurrences[] = $file . ':' . implode(';', $lines); |
| 580 |
} |
| 581 |
$occurrences = implode("\n#: ", $occurrences); |
| 582 |
$header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n"; |
| 583 |
|
| 584 |
$sentence = ''; |
| 585 |
if ($context) { |
| 586 |
$sentence .= "msgctxt \"{$context}\"\n"; |
| 587 |
} |
| 588 |
if ($plural === false) { |
| 589 |
$sentence .= "msgid \"{$msgid}\"\n"; |
| 590 |
$sentence .= "msgstr \"\"\n\n"; |
| 591 |
} else {
|
| 592 |
$sentence .= "msgid \"{$msgid}\"\n"; |
| 593 |
$sentence .= "msgid_plural \"{$plural}\"\n"; |
| 594 |
$sentence .= "msgstr[0] \"\"\n"; |
| 595 |
$sentence .= "msgstr[1] \"\"\n\n"; |
| 596 |
} |
| 597 |
|
| 598 |
$this->_store($category, $domain, $header, $sentence); |
| 599 |
if (($category !== 'LC_MESSAGES' || $domain !== 'default') && $this->_merge) { |
| 600 |
$this->_store('LC_MESSAGES', 'default', $header, $sentence); |
| 601 |
} |
| 602 |
} |
| 603 |
} |
| 604 |
} |
| 605 |
} |
| 606 |
} |
| 607 |
|
| 608 |
/**
|
| 609 |
* Prepare a file to be stored
|
| 610 |
*
|
| 611 |
* @param string $category The category
|
| 612 |
* @param string $domain The domain
|
| 613 |
* @param string $header The header content.
|
| 614 |
* @param string $sentence The sentence to store.
|
| 615 |
* @return void
|
| 616 |
*/
|
| 617 |
protected function _store($category, $domain, $header, $sentence) { |
| 618 |
if (!isset($this->_storage[$category])) { |
| 619 |
$this->_storage[$category] = array(); |
| 620 |
} |
| 621 |
if (!isset($this->_storage[$category][$domain])) { |
| 622 |
$this->_storage[$category][$domain] = array(); |
| 623 |
} |
| 624 |
if (!isset($this->_storage[$category][$domain][$sentence])) { |
| 625 |
$this->_storage[$category][$domain][$sentence] = $header; |
| 626 |
} else {
|
| 627 |
$this->_storage[$category][$domain][$sentence] .= $header; |
| 628 |
} |
| 629 |
} |
| 630 |
|
| 631 |
/**
|
| 632 |
* Write the files that need to be stored
|
| 633 |
*
|
| 634 |
* @return void
|
| 635 |
*/
|
| 636 |
protected function _writeFiles() { |
| 637 |
$overwriteAll = false; |
| 638 |
if (!empty($this->params['overwrite'])) { |
| 639 |
$overwriteAll = true; |
| 640 |
} |
| 641 |
foreach ($this->_storage as $category => $domains) { |
| 642 |
foreach ($domains as $domain => $sentences) { |
| 643 |
$output = $this->_writeHeader(); |
| 644 |
foreach ($sentences as $sentence => $header) { |
| 645 |
$output .= $header . $sentence; |
| 646 |
} |
| 647 |
|
| 648 |
$filename = $domain . '.pot'; |
| 649 |
if ($category === 'LC_MESSAGES') { |
| 650 |
$File = new File($this->_output . $filename); |
| 651 |
} else {
|
| 652 |
new Folder($this->_output . $category, true); |
| 653 |
$File = new File($this->_output . $category . DS . $filename); |
| 654 |
} |
| 655 |
$response = ''; |
| 656 |
while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') { |
| 657 |
$this->out();
|
| 658 |
$response = $this->in( |
| 659 |
__d('cake_console', 'Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename), |
| 660 |
array('y', 'n', 'a'), |
| 661 |
'y'
|
| 662 |
); |
| 663 |
if (strtoupper($response) === 'N') { |
| 664 |
$response = ''; |
| 665 |
while (!$response) { |
| 666 |
$response = $this->in(__d('cake_console', "What would you like to name this file?"), null, 'new_' . $filename); |
| 667 |
$File = new File($this->_output . $response); |
| 668 |
$filename = $response; |
| 669 |
} |
| 670 |
} elseif (strtoupper($response) === 'A') { |
| 671 |
$overwriteAll = true; |
| 672 |
} |
| 673 |
} |
| 674 |
$File->write($output); |
| 675 |
$File->close();
|
| 676 |
} |
| 677 |
} |
| 678 |
} |
| 679 |
|
| 680 |
/**
|
| 681 |
* Build the translation template header
|
| 682 |
*
|
| 683 |
* @return string Translation template header
|
| 684 |
*/
|
| 685 |
protected function _writeHeader() { |
| 686 |
$output = "# LANGUAGE translation of CakePHP Application\n"; |
| 687 |
$output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n"; |
| 688 |
$output .= "#\n"; |
| 689 |
$output .= "#, fuzzy\n"; |
| 690 |
$output .= "msgid \"\"\n"; |
| 691 |
$output .= "msgstr \"\"\n"; |
| 692 |
$output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; |
| 693 |
$output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; |
| 694 |
$output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n"; |
| 695 |
$output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n"; |
| 696 |
$output .= "\"MIME-Version: 1.0\\n\"\n"; |
| 697 |
$output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; |
| 698 |
$output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; |
| 699 |
$output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n"; |
| 700 |
return $output; |
| 701 |
} |
| 702 |
|
| 703 |
/**
|
| 704 |
* Get the strings from the position forward
|
| 705 |
*
|
| 706 |
* @param int &$position Actual position on tokens array
|
| 707 |
* @param int $target Number of strings to extract
|
| 708 |
* @return array Strings extracted
|
| 709 |
*/
|
| 710 |
protected function _getStrings(&$position, $target) { |
| 711 |
$strings = array(); |
| 712 |
$count = count($strings); |
| 713 |
while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) { |
| 714 |
$count = count($strings); |
| 715 |
if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') { |
| 716 |
$string = ''; |
| 717 |
while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') { |
| 718 |
if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { |
| 719 |
$string .= $this->_formatString($this->_tokens[$position][1]); |
| 720 |
} |
| 721 |
$position++;
|
| 722 |
} |
| 723 |
$strings[] = $string; |
| 724 |
} elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { |
| 725 |
$strings[] = $this->_formatString($this->_tokens[$position][1]); |
| 726 |
} elseif ($this->_tokens[$position][0] == T_LNUMBER) { |
| 727 |
$strings[] = $this->_tokens[$position][1]; |
| 728 |
} |
| 729 |
$position++;
|
| 730 |
} |
| 731 |
return $strings; |
| 732 |
} |
| 733 |
|
| 734 |
/**
|
| 735 |
* Format a string to be added as a translatable string
|
| 736 |
*
|
| 737 |
* @param string $string String to format
|
| 738 |
* @return string Formatted string
|
| 739 |
*/
|
| 740 |
protected function _formatString($string) { |
| 741 |
$quote = substr($string, 0, 1); |
| 742 |
$string = substr($string, 1, -1); |
| 743 |
if ($quote === '"') { |
| 744 |
$string = stripcslashes($string); |
| 745 |
} else {
|
| 746 |
$string = strtr($string, array("\\'" => "'", "\\\\" => "\\")); |
| 747 |
} |
| 748 |
$string = str_replace("\r\n", "\n", $string); |
| 749 |
return addcslashes($string, "\0..\37\\\""); |
| 750 |
} |
| 751 |
|
| 752 |
/**
|
| 753 |
* Indicate an invalid marker on a processed file
|
| 754 |
*
|
| 755 |
* @param string $file File where invalid marker resides
|
| 756 |
* @param int $line Line number
|
| 757 |
* @param string $marker Marker found
|
| 758 |
* @param int $count Count
|
| 759 |
* @return void
|
| 760 |
*/
|
| 761 |
protected function _markerError($file, $line, $marker, $count) { |
| 762 |
$this->err(__d('cake_console', "Invalid marker content in %s:%s\n* %s(", $file, $line, $marker)); |
| 763 |
$count += 2; |
| 764 |
$tokenCount = count($this->_tokens); |
| 765 |
$parenthesis = 1; |
| 766 |
|
| 767 |
while ((($tokenCount - $count) > 0) && $parenthesis) { |
| 768 |
if (is_array($this->_tokens[$count])) { |
| 769 |
$this->err($this->_tokens[$count][1], false); |
| 770 |
} else {
|
| 771 |
$this->err($this->_tokens[$count], false); |
| 772 |
if ($this->_tokens[$count] === '(') { |
| 773 |
$parenthesis++;
|
| 774 |
} |
| 775 |
|
| 776 |
if ($this->_tokens[$count] === ')') { |
| 777 |
$parenthesis--;
|
| 778 |
} |
| 779 |
} |
| 780 |
$count++;
|
| 781 |
} |
| 782 |
$this->err("\n", true); |
| 783 |
} |
| 784 |
|
| 785 |
/**
|
| 786 |
* Search files that may contain translatable strings
|
| 787 |
*
|
| 788 |
* @return void
|
| 789 |
*/
|
| 790 |
protected function _searchFiles() { |
| 791 |
$pattern = false; |
| 792 |
if (!empty($this->_exclude)) { |
| 793 |
$exclude = array(); |
| 794 |
foreach ($this->_exclude as $e) { |
| 795 |
if (DS !== '\\' && $e[0] !== DS) { |
| 796 |
$e = DS . $e; |
| 797 |
} |
| 798 |
$exclude[] = preg_quote($e, '/'); |
| 799 |
} |
| 800 |
$pattern = '/' . implode('|', $exclude) . '/'; |
| 801 |
} |
| 802 |
foreach ($this->_paths as $path) { |
| 803 |
$Folder = new Folder($path); |
| 804 |
$files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true); |
| 805 |
if (!empty($pattern)) { |
| 806 |
foreach ($files as $i => $file) { |
| 807 |
if (preg_match($pattern, $file)) { |
| 808 |
unset($files[$i]); |
| 809 |
} |
| 810 |
} |
| 811 |
$files = array_values($files); |
| 812 |
} |
| 813 |
$this->_files = array_merge($this->_files, $files); |
| 814 |
} |
| 815 |
} |
| 816 |
|
| 817 |
/**
|
| 818 |
* Returns whether this execution is meant to extract string only from directories in folder represented by the
|
| 819 |
* APP constant, i.e. this task is extracting strings from same application.
|
| 820 |
*
|
| 821 |
* @return bool
|
| 822 |
*/
|
| 823 |
protected function _isExtractingApp() { |
| 824 |
return $this->_paths === array(APP); |
| 825 |
} |
| 826 |
|
| 827 |
/**
|
| 828 |
* Checks whether or not a given path is usable for writing.
|
| 829 |
*
|
| 830 |
* @param string $path Path to folder
|
| 831 |
* @return bool true if it exists and is writable, false otherwise
|
| 832 |
*/
|
| 833 |
protected function _isPathUsable($path) { |
| 834 |
return is_dir($path) && is_writable($path); |
| 835 |
} |
| 836 |
} |