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