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