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