1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
18:
19: App::uses('AppShell', 'Console/Command');
20: App::uses('File', 'Utility');
21: App::uses('Folder', 'Utility');
22:
23: 24: 25: 26: 27:
28: class ExtractTask extends AppShell {
29:
30: 31: 32: 33: 34:
35: protected $_paths = array();
36:
37: 38: 39: 40: 41:
42: protected $_files = array();
43:
44: 45: 46: 47: 48:
49: protected $_merge = false;
50:
51: 52: 53: 54: 55:
56: protected $_file = null;
57:
58: 59: 60: 61: 62:
63: protected $_storage = array();
64:
65: 66: 67: 68: 69:
70: protected $_tokens = array();
71:
72: 73: 74: 75: 76:
77: protected $_translations = array();
78:
79: 80: 81: 82: 83:
84: protected $_output = null;
85:
86: 87: 88: 89: 90:
91: protected $_exclude = array();
92:
93: 94: 95: 96: 97:
98: protected $_extractValidation = true;
99:
100: 101: 102: 103: 104:
105: protected $_validationDomain = 'default';
106:
107: 108: 109: 110: 111:
112: protected function _getPaths() {
113: $defaultPath = APP;
114: while (true) {
115: $currentPaths = count($this->_paths) > 0 ? $this->_paths : array('None');
116: $message = __d(
117: 'cake_console',
118: "Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one",
119: implode(', ', $currentPaths)
120: );
121: $response = $this->in($message, null, $defaultPath);
122: if (strtoupper($response) === 'Q') {
123: $this->out(__d('cake_console', 'Extract Aborted'));
124: return $this->_stop();
125: } elseif (strtoupper($response) === 'D' && count($this->_paths)) {
126: $this->out();
127: return;
128: } elseif (strtoupper($response) === 'D') {
129: $this->err(__d('cake_console', '<warning>No directories selected.</warning> Please choose a directory.'));
130: } elseif (is_dir($response)) {
131: $this->_paths[] = $response;
132: $defaultPath = 'D';
133: } else {
134: $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.'));
135: }
136: $this->out();
137: }
138: }
139: 140: 141: 142: 143:
144: public function execute() {
145: if (!empty($this->params['exclude'])) {
146: $this->_exclude = explode(',', $this->params['exclude']);
147: }
148: if (isset($this->params['files']) && !is_array($this->params['files'])) {
149: $this->_files = explode(',', $this->params['files']);
150: }
151: if (isset($this->params['paths'])) {
152: $this->_paths = explode(',', $this->params['paths']);
153: } elseif (isset($this->params['plugin'])) {
154: $plugin = Inflector::camelize($this->params['plugin']);
155: if (!CakePlugin::loaded($plugin)) {
156: CakePlugin::load($plugin);
157: }
158: $this->_paths = array(CakePlugin::path($plugin));
159: $this->params['plugin'] = $plugin;
160: } else {
161: $this->_getPaths();
162: }
163:
164: if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) {
165: $this->_exclude = array_merge($this->_exclude, App::path('plugins'));
166: }
167:
168: if (!empty($this->params['ignore-model-validation']) || (!$this->_isExtractingApp() && empty($plugin))) {
169: $this->_extractValidation = false;
170: }
171: if (!empty($this->params['validation-domain'])) {
172: $this->_validationDomain = $this->params['validation-domain'];
173: }
174:
175: if (isset($this->params['output'])) {
176: $this->_output = $this->params['output'];
177: } elseif (isset($this->params['plugin'])) {
178: $this->_output = $this->_paths[0] . DS . 'Locale';
179: } else {
180: $message = __d('cake_console', "What is the path you would like to output?\n[Q]uit", $this->_paths[0] . DS . 'Locale');
181: while (true) {
182: $response = $this->in($message, null, rtrim($this->_paths[0], DS) . DS . 'Locale');
183: if (strtoupper($response) === 'Q') {
184: $this->out(__d('cake_console', 'Extract Aborted'));
185: $this->_stop();
186: } elseif (is_dir($response)) {
187: $this->_output = $response . DS;
188: break;
189: } else {
190: $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.'));
191: }
192: $this->out();
193: }
194: }
195:
196: if (isset($this->params['merge'])) {
197: $this->_merge = !(strtolower($this->params['merge']) === 'no');
198: } else {
199: $this->out();
200: $response = $this->in(__d('cake_console', 'Would you like to merge all domains strings into the default.pot file?'), array('y', 'n'), 'n');
201: $this->_merge = strtolower($response) === 'y';
202: }
203:
204: if (empty($this->_files)) {
205: $this->_searchFiles();
206: }
207: $this->_output = rtrim($this->_output, DS) . DS;
208: $this->_extract();
209: }
210:
211: 212: 213: 214: 215: 216: 217: 218: 219:
220: protected function _addTranslation($domain, $msgid, $details = array()) {
221: if (empty($this->_translations[$domain][$msgid])) {
222: $this->_translations[$domain][$msgid] = array(
223: 'msgid_plural' => false
224: );
225: }
226:
227: if (isset($details['msgid_plural'])) {
228: $this->_translations[$domain][$msgid]['msgid_plural'] = $details['msgid_plural'];
229: }
230:
231: if (isset($details['file'])) {
232: $line = 0;
233: if (isset($details['line'])) {
234: $line = $details['line'];
235: }
236: $this->_translations[$domain][$msgid]['references'][$details['file']][] = $line;
237: }
238: }
239:
240: 241: 242: 243: 244:
245: protected function _extract() {
246: $this->out();
247: $this->out();
248: $this->out(__d('cake_console', 'Extracting...'));
249: $this->hr();
250: $this->out(__d('cake_console', 'Paths:'));
251: foreach ($this->_paths as $path) {
252: $this->out(' ' . $path);
253: }
254: $this->out(__d('cake_console', 'Output Directory: ') . $this->_output);
255: $this->hr();
256: $this->_extractTokens();
257: $this->_extractValidationMessages();
258: $this->_buildFiles();
259: $this->_writeFiles();
260: $this->_paths = $this->_files = $this->_storage = array();
261: $this->_translations = $this->_tokens = array();
262: $this->_extractValidation = true;
263: $this->out();
264: $this->out(__d('cake_console', 'Done.'));
265: }
266:
267: 268: 269: 270: 271:
272: public function getOptionParser() {
273: $parser = parent::getOptionParser();
274: return $parser->description(__d('cake_console', 'CakePHP Language String Extraction:'))
275: ->addOption('app', array('help' => __d('cake_console', 'Directory where your application is located.')))
276: ->addOption('paths', array('help' => __d('cake_console', 'Comma separated list of paths.')))
277: ->addOption('merge', array(
278: 'help' => __d('cake_console', 'Merge all domain strings into the default.po file.'),
279: 'choices' => array('yes', 'no')
280: ))
281: ->addOption('output', array('help' => __d('cake_console', 'Full path to output directory.')))
282: ->addOption('files', array('help' => __d('cake_console', 'Comma separated list of files.')))
283: ->addOption('exclude-plugins', array(
284: 'boolean' => true,
285: 'default' => true,
286: 'help' => __d('cake_console', 'Ignores all files in plugins if this command is run inside from the same app directory.')
287: ))
288: ->addOption('plugin', array(
289: 'help' => __d('cake_console', 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.')
290: ))
291: ->addOption('ignore-model-validation', array(
292: 'boolean' => true,
293: 'default' => false,
294: 'help' => __d('cake_console', 'Ignores validation messages in the $validate property.' .
295: ' If this flag is not set and the command is run from the same app directory,' .
296: ' all messages in model validation rules will be extracted as tokens.')
297: ))
298: ->addOption('validation-domain', array(
299: 'help' => __d('cake_console', 'If set to a value, the localization domain to be used for model validation messages.')
300: ))
301: ->addOption('exclude', array(
302: 'help' => __d('cake_console', 'Comma separated list of directories to exclude.' .
303: ' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors')
304: ));
305: }
306:
307: 308: 309: 310: 311:
312: protected function _extractTokens() {
313: foreach ($this->_files as $file) {
314: $this->_file = $file;
315: $this->out(__d('cake_console', 'Processing %s...', $file));
316:
317: $code = file_get_contents($file);
318: $allTokens = token_get_all($code);
319:
320: $this->_tokens = array();
321: foreach ($allTokens as $token) {
322: if (!is_array($token) || ($token[0] != T_WHITESPACE && $token[0] != T_INLINE_HTML)) {
323: $this->_tokens[] = $token;
324: }
325: }
326: unset($allTokens);
327: $this->_parse('__', array('singular'));
328: $this->_parse('__n', array('singular', 'plural'));
329: $this->_parse('__d', array('domain', 'singular'));
330: $this->_parse('__c', array('singular'));
331: $this->_parse('__dc', array('domain', 'singular'));
332: $this->_parse('__dn', array('domain', 'singular', 'plural'));
333: $this->_parse('__dcn', array('domain', 'singular', 'plural'));
334: }
335: }
336:
337: 338: 339: 340: 341: 342: 343:
344: protected function _parse($functionName, $map) {
345: $count = 0;
346: $tokenCount = count($this->_tokens);
347:
348: while (($tokenCount - $count) > 1) {
349: $countToken = $this->_tokens[$count];
350: $firstParenthesis = $this->_tokens[$count + 1];
351: if (!is_array($countToken)) {
352: $count++;
353: continue;
354: }
355:
356: list($type, $string, $line) = $countToken;
357: if (($type == T_STRING) && ($string == $functionName) && ($firstParenthesis == '(')) {
358: $position = $count;
359: $depth = 0;
360:
361: while ($depth == 0) {
362: if ($this->_tokens[$position] == '(') {
363: $depth++;
364: } elseif ($this->_tokens[$position] == ')') {
365: $depth--;
366: }
367: $position++;
368: }
369:
370: $mapCount = count($map);
371: $strings = $this->_getStrings($position, $mapCount);
372:
373: if ($mapCount == count($strings)) {
374: extract(array_combine($map, $strings));
375: $domain = isset($domain) ? $domain : 'default';
376: $details = array(
377: 'file' => $this->_file,
378: 'line' => $line,
379: );
380: if (isset($plural)) {
381: $details['msgid_plural'] = $plural;
382: }
383: $this->_addTranslation($domain, $singular, $details);
384: } else {
385: $this->_markerError($this->_file, $line, $functionName, $count);
386: }
387: }
388: $count++;
389: }
390: }
391:
392: 393: 394: 395: 396: 397:
398: protected function _extractValidationMessages() {
399: if (!$this->_extractValidation) {
400: return;
401: }
402:
403: App::uses('AppModel', 'Model');
404: $plugin = null;
405: if (!empty($this->params['plugin'])) {
406: App::uses($this->params['plugin'] . 'AppModel', $this->params['plugin'] . '.Model');
407: $plugin = $this->params['plugin'] . '.';
408: }
409: $models = App::objects($plugin . 'Model', null, false);
410:
411: foreach ($models as $model) {
412: App::uses($model, $plugin . 'Model');
413: $reflection = new ReflectionClass($model);
414: if (!$reflection->isSubClassOf('Model')) {
415: continue;
416: }
417: $properties = $reflection->getDefaultProperties();
418: $validate = $properties['validate'];
419: if (empty($validate)) {
420: continue;
421: }
422:
423: $file = $reflection->getFileName();
424: $domain = $this->_validationDomain;
425: if (!empty($properties['validationDomain'])) {
426: $domain = $properties['validationDomain'];
427: }
428: foreach ($validate as $field => $rules) {
429: $this->_processValidationRules($field, $rules, $file, $domain);
430: }
431: }
432: }
433:
434: 435: 436: 437: 438: 439: 440: 441: 442: 443:
444: protected function _processValidationRules($field, $rules, $file, $domain) {
445: if (!is_array($rules)) {
446: return;
447: }
448:
449: $dims = Set::countDim($rules);
450: if ($dims == 1 || ($dims == 2 && isset($rules['message']))) {
451: $rules = array($rules);
452: }
453:
454: foreach ($rules as $rule => $validateProp) {
455: $msgid = null;
456: if (isset($validateProp['message'])) {
457: if (is_array($validateProp['message'])) {
458: $msgid = $validateProp['message'][0];
459: } else {
460: $msgid = $validateProp['message'];
461: }
462: } elseif (is_string($rule)) {
463: $msgid = $rule;
464: }
465: if ($msgid) {
466: $details = array(
467: 'file' => $file,
468: 'line' => 'validation for field ' . $field
469: );
470: $this->_addTranslation($domain, $msgid, $details);
471: }
472: }
473: }
474:
475: 476: 477: 478: 479:
480: protected function _buildFiles() {
481: $paths = $this->_paths;
482: $paths[] = realpath(APP) . DS;
483: foreach ($this->_translations as $domain => $translations) {
484: foreach ($translations as $msgid => $details) {
485: $plural = $details['msgid_plural'];
486: $files = $details['references'];
487: $occurrences = array();
488: foreach ($files as $file => $lines) {
489: $lines = array_unique($lines);
490: $occurrences[] = $file . ':' . implode(';', $lines);
491: }
492: $occurrences = implode("\n#: ", $occurrences);
493: $header = '#: ' . str_replace($paths, '', $occurrences) . "\n";
494:
495: if ($plural === false) {
496: $sentence = "msgid \"{$msgid}\"\n";
497: $sentence .= "msgstr \"\"\n\n";
498: } else {
499: $sentence = "msgid \"{$msgid}\"\n";
500: $sentence .= "msgid_plural \"{$plural}\"\n";
501: $sentence .= "msgstr[0] \"\"\n";
502: $sentence .= "msgstr[1] \"\"\n\n";
503: }
504:
505: $this->_store($domain, $header, $sentence);
506: if ($domain != 'default' && $this->_merge) {
507: $this->_store('default', $header, $sentence);
508: }
509: }
510: }
511: }
512:
513: 514: 515: 516: 517: 518: 519: 520:
521: protected function _store($domain, $header, $sentence) {
522: if (!isset($this->_storage[$domain])) {
523: $this->_storage[$domain] = array();
524: }
525: if (!isset($this->_storage[$domain][$sentence])) {
526: $this->_storage[$domain][$sentence] = $header;
527: } else {
528: $this->_storage[$domain][$sentence] .= $header;
529: }
530: }
531:
532: 533: 534: 535: 536:
537: protected function _writeFiles() {
538: $overwriteAll = false;
539: foreach ($this->_storage as $domain => $sentences) {
540: $output = $this->_writeHeader();
541: foreach ($sentences as $sentence => $header) {
542: $output .= $header . $sentence;
543: }
544:
545: $filename = $domain . '.pot';
546: $File = new File($this->_output . $filename);
547: $response = '';
548: while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') {
549: $this->out();
550: $response = $this->in(
551: __d('cake_console', 'Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename),
552: array('y', 'n', 'a'),
553: 'y'
554: );
555: if (strtoupper($response) === 'N') {
556: $response = '';
557: while ($response == '') {
558: $response = $this->in(__d('cake_console', "What would you like to name this file?"), null, 'new_' . $filename);
559: $File = new File($this->_output . $response);
560: $filename = $response;
561: }
562: } elseif (strtoupper($response) === 'A') {
563: $overwriteAll = true;
564: }
565: }
566: $File->write($output);
567: $File->close();
568: }
569: }
570:
571: 572: 573: 574: 575:
576: protected function _writeHeader() {
577: $output = "# LANGUAGE translation of CakePHP Application\n";
578: $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
579: $output .= "#\n";
580: $output .= "#, fuzzy\n";
581: $output .= "msgid \"\"\n";
582: $output .= "msgstr \"\"\n";
583: $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
584: $output .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
585: $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
586: $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
587: $output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
588: $output .= "\"MIME-Version: 1.0\\n\"\n";
589: $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
590: $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
591: $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
592: return $output;
593: }
594:
595: 596: 597: 598: 599: 600: 601:
602: protected function _getStrings(&$position, $target) {
603: $strings = array();
604: $count = count($strings);
605: while ($count < $target && ($this->_tokens[$position] == ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING)) {
606: $count = count($strings);
607: if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] == '.') {
608: $string = '';
609: while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] == '.') {
610: if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
611: $string .= $this->_formatString($this->_tokens[$position][1]);
612: }
613: $position++;
614: }
615: $strings[] = $string;
616: } elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
617: $strings[] = $this->_formatString($this->_tokens[$position][1]);
618: }
619: $position++;
620: }
621: return $strings;
622: }
623:
624: 625: 626: 627: 628: 629:
630: protected function _formatString($string) {
631: $quote = substr($string, 0, 1);
632: $string = substr($string, 1, -1);
633: if ($quote == '"') {
634: $string = stripcslashes($string);
635: } else {
636: $string = strtr($string, array("\\'" => "'", "\\\\" => "\\"));
637: }
638: $string = str_replace("\r\n", "\n", $string);
639: return addcslashes($string, "\0..\37\\\"");
640: }
641:
642: 643: 644: 645: 646: 647: 648: 649: 650:
651: protected function _markerError($file, $line, $marker, $count) {
652: $this->out(__d('cake_console', "Invalid marker content in %s:%s\n* %s(", $file, $line, $marker), true);
653: $count += 2;
654: $tokenCount = count($this->_tokens);
655: $parenthesis = 1;
656:
657: while ((($tokenCount - $count) > 0) && $parenthesis) {
658: if (is_array($this->_tokens[$count])) {
659: $this->out($this->_tokens[$count][1], false);
660: } else {
661: $this->out($this->_tokens[$count], false);
662: if ($this->_tokens[$count] == '(') {
663: $parenthesis++;
664: }
665:
666: if ($this->_tokens[$count] == ')') {
667: $parenthesis--;
668: }
669: }
670: $count++;
671: }
672: $this->out("\n", true);
673: }
674:
675: 676: 677: 678: 679:
680: protected function _searchFiles() {
681: $pattern = false;
682: if (!empty($this->_exclude)) {
683: $exclude = array();
684: foreach ($this->_exclude as $e) {
685: if (DS !== '\\' && $e[0] !== DS) {
686: $e = DS . $e;
687: }
688: $exclude[] = preg_quote($e, '/');
689: }
690: $pattern = '/' . implode('|', $exclude) . '/';
691: }
692: foreach ($this->_paths as $path) {
693: $Folder = new Folder($path);
694: $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true);
695: if (!empty($pattern)) {
696: foreach ($files as $i => $file) {
697: if (preg_match($pattern, $file)) {
698: unset($files[$i]);
699: }
700: }
701: $files = array_values($files);
702: }
703: $this->_files = array_merge($this->_files, $files);
704: }
705: }
706:
707: 708: 709: 710: 711: 712:
713: protected function _isExtractingApp() {
714: return $this->_paths === array(APP);
715: }
716:
717: }
718: