pictcode / lib / Cake / Console / Command / Task / ExtractTask.php @ a0ff9cef
履歴 | 表示 | アノテート | ダウンロード (24.812 KB)
| 1 | 635eef61 | spyder1211 | <?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 | } |