source: Dev/branches/cakephp/cake/console/libs/tasks/extract.php @ 126

Last change on this file since 126 was 126, checked in by fpvanagthoven, 14 years ago

Cakephp branch.

File size: 14.6 KB
Line 
1<?php
2/**
3 * Language string extractor
4 *
5 * PHP versions 4 and 5
6 *
7 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8 * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
9 *
10 * Licensed under The MIT License
11 * Redistributions of files must retain the above copyright notice.
12 *
13 * @copyright     Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
14 * @link          http://cakephp.org CakePHP(tm) Project
15 * @package       cake
16 * @subpackage    cake.cake.console.libs
17 * @since         CakePHP(tm) v 1.2.0.5012
18 * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
19 */
20
21/**
22 * Language string extractor
23 *
24 * @package       cake
25 * @subpackage    cake.cake.console.libs.tasks
26 */
27class ExtractTask extends Shell {
28
29/**
30 * Paths to use when looking for strings
31 *
32 * @var string
33 * @access private
34 */
35        var $__paths = array();
36
37/**
38 * Files from where to extract
39 *
40 * @var array
41 * @access private
42 */
43        var $__files = array();
44
45/**
46 * Merge all domains string into the default.pot file
47 *
48 * @var boolean
49 * @access private
50 */
51        var $__merge = false;
52
53/**
54 * Current file being processed
55 *
56 * @var string
57 * @access private
58 */
59        var $__file = null;
60
61/**
62 * Contains all content waiting to be write
63 *
64 * @var string
65 * @access private
66 */
67        var $__storage = array();
68
69/**
70 * Extracted tokens
71 *
72 * @var array
73 * @access private
74 */
75        var $__tokens = array();
76
77/**
78 * Extracted strings
79 *
80 * @var array
81 * @access private
82 */
83        var $__strings = array();
84
85/**
86 * Destination path
87 *
88 * @var string
89 * @access private
90 */
91        var $__output = null;
92
93/**
94 * Execution method always used for tasks
95 *
96 * @return void
97 * @access private
98 */
99        function execute() {
100                if (isset($this->params['files']) && !is_array($this->params['files'])) {
101                        $this->__files = explode(',', $this->params['files']);
102                }
103                if (isset($this->params['paths'])) {
104                        $this->__paths = explode(',', $this->params['paths']);
105                } else {
106                        $defaultPath = $this->params['working'];
107                        $message = sprintf(__("What is the full path you would like to extract?\nExample: %s\n[Q]uit [D]one", true), $this->params['root'] . DS . 'myapp');
108                        while (true) {
109                                $response = $this->in($message, null, $defaultPath);
110                                if (strtoupper($response) === 'Q') {
111                                        $this->out(__('Extract Aborted', true));
112                                        $this->_stop();
113                                } elseif (strtoupper($response) === 'D') {
114                                        $this->out();
115                                        break;
116                                } elseif (is_dir($response)) {
117                                        $this->__paths[] = $response;
118                                        $defaultPath = 'D';
119                                } else {
120                                        $this->err(__('The directory path you supplied was not found. Please try again.', true));
121                                }
122                                $this->out();
123                        }
124                }
125
126                if (isset($this->params['output'])) {
127                        $this->__output = $this->params['output'];
128                } else {
129                        $message = sprintf(__("What is the full path you would like to output?\nExample: %s\n[Q]uit", true), $this->__paths[0] . DS . 'locale');
130                        while (true) {
131                                $response = $this->in($message, null, $this->__paths[0] . DS . 'locale');
132                                if (strtoupper($response) === 'Q') {
133                                        $this->out(__('Extract Aborted', true));
134                                        $this->_stop();
135                                } elseif (is_dir($response)) {
136                                        $this->__output = $response . DS;
137                                        break;
138                                } else {
139                                        $this->err(__('The directory path you supplied was not found. Please try again.', true));
140                                }
141                                $this->out();
142                        }
143                }
144
145                if (isset($this->params['merge'])) {
146                        $this->__merge = !(strtolower($this->params['merge']) === 'no');
147                } else {
148                        $this->out();
149                        $response = $this->in(sprintf(__('Would you like to merge all domains strings into the default.pot file?', true)), array('y', 'n'), 'n');
150                        $this->__merge = strtolower($response) === 'y';
151                }
152
153                if (empty($this->__files)) {
154                        $this->__searchFiles();
155                }
156                $this->__extract();
157        }
158
159/**
160 * Extract text
161 *
162 * @return void
163 * @access private
164 */
165        function __extract() {
166                $this->out();
167                $this->out();
168                $this->out(__('Extracting...', true));
169                $this->hr();
170                $this->out(__('Paths:', true));
171                foreach ($this->__paths as $path) {
172                        $this->out('   ' . $path);
173                }
174                $this->out(__('Output Directory: ', true) . $this->__output);
175                $this->hr();
176                $this->__extractTokens();
177                $this->__buildFiles();
178                $this->__writeFiles();
179                $this->__paths = $this->__files = $this->__storage = array();
180                $this->__strings = $this->__tokens = array();
181                $this->out();
182                $this->out(__('Done.', true));
183        }
184
185/**
186 * Show help options
187 *
188 * @return void
189 * @access public
190 */
191        function help() {
192                $this->out(__('CakePHP Language String Extraction:', true));
193                $this->hr();
194                $this->out(__('The Extract script generates .pot file(s) with translations', true));
195                $this->out(__('By default the .pot file(s) will be place in the locale directory of -app', true));
196                $this->out(__('By default -app is ROOT/app', true));
197                $this->hr();
198                $this->out(__('Usage: cake i18n extract <command> <param1> <param2>...', true));
199                $this->out();
200                $this->out(__('Params:', true));
201                $this->out(__('   -app [path...]: directory where your application is located', true));
202                $this->out(__('   -root [path...]: path to install', true));
203                $this->out(__('   -core [path...]: path to cake directory', true));
204                $this->out(__('   -paths [comma separated list of paths, full path is needed]', true));
205                $this->out(__('   -merge [yes|no]: Merge all domains strings into the default.pot file', true));
206                $this->out(__('   -output [path...]: Full path to output directory', true));
207                $this->out(__('   -files: [comma separated list of files, full path to file is needed]', true));
208                $this->out();
209                $this->out(__('Commands:', true));
210                $this->out(__('   cake i18n extract help: Shows this help message.', true));
211                $this->out();
212        }
213
214/**
215 * Extract tokens out of all files to be processed
216 *
217 * @return void
218 * @access private
219 */
220        function __extractTokens() {
221                foreach ($this->__files as $file) {
222                        $this->__file = $file;
223                        $this->out(sprintf(__('Processing %s...', true), $file));
224
225                        $code = file_get_contents($file);
226                        $allTokens = token_get_all($code);
227                        $this->__tokens = array();
228                        $lineNumber = 1;
229
230                        foreach ($allTokens as $token) {
231                                if ((!is_array($token)) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) {
232                                        if (is_array($token)) {
233                                                $token[] = $lineNumber;
234                                        }
235                                        $this->__tokens[] = $token;
236                                }
237
238                                if (is_array($token)) {
239                                        $lineNumber += count(explode("\n", $token[1])) - 1;
240                                } else {
241                                        $lineNumber += count(explode("\n", $token)) - 1;
242                                }
243                        }
244                        unset($allTokens);
245                        $this->__parse('__', array('singular'));
246                        $this->__parse('__n', array('singular', 'plural'));
247                        $this->__parse('__d', array('domain', 'singular'));
248                        $this->__parse('__c', array('singular'));
249                        $this->__parse('__dc', array('domain', 'singular'));
250                        $this->__parse('__dn', array('domain', 'singular', 'plural'));
251                        $this->__parse('__dcn', array('domain', 'singular', 'plural'));
252                }
253        }
254
255/**
256 * Parse tokens
257 *
258 * @param string $functionName Function name that indicates translatable string (e.g: '__')
259 * @param array $map Array containing what variables it will find (e.g: domain, singular, plural)
260 * @return void
261 * @access private
262 */
263        function __parse($functionName, $map) {
264                $count = 0;
265                $tokenCount = count($this->__tokens);
266
267                while (($tokenCount - $count) > 1) {
268                        list($countToken, $firstParenthesis) = array($this->__tokens[$count], $this->__tokens[$count + 1]);
269                        if (!is_array($countToken)) {
270                                $count++;
271                                continue;
272                        }
273
274                        list($type, $string, $line) = $countToken;
275                        if (($type == T_STRING) && ($string == $functionName) && ($firstParenthesis == '(')) {
276                                $position = $count;
277                                $depth = 0;
278
279                                while ($depth == 0) {
280                                        if ($this->__tokens[$position] == '(') {
281                                                $depth++;
282                                        } elseif ($this->__tokens[$position] == ')') {
283                                                $depth--;
284                                        }
285                                        $position++;
286                                }
287
288                                $mapCount = count($map);
289                                $strings = $this->__getStrings($position, $mapCount);
290
291                                if ($mapCount == count($strings)) {
292                                        extract(array_combine($map, $strings));
293                                        $domain = isset($domain) ? $domain : 'default';
294                                        $string = isset($plural) ? $singular . "\0" . $plural : $singular;
295                                        $this->__strings[$domain][$string][$this->__file][] = $line;
296                                } else {
297                                        $this->__markerError($this->__file, $line, $functionName, $count);
298                                }
299                        }
300                        $count++;
301                }
302        }
303
304/**
305* Get the strings from the position forward
306*
307* @param integer $position Actual position on tokens array
308* @param integer $target Number of strings to extract
309* @return array Strings extracted
310*/
311        function __getStrings($position, $target) {
312                $strings = array();
313                while (count($strings) < $target && ($this->__tokens[$position] == ',' || $this->__tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING)) {
314                        $condition1 = ($this->__tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->__tokens[$position+1] == '.');
315                        $condition2 = ($this->__tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->__tokens[$position+1][0] == T_COMMENT);
316                        if ($condition1 || $condition2) {
317                                $string = '';
318                                while ($this->__tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->__tokens[$position][0] == T_COMMENT || $this->__tokens[$position] == '.') {
319                                        if ($this->__tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
320                                                $string .= $this->__formatString($this->__tokens[$position][1]);
321                                        }
322                                        $position++;
323                                }
324                                if ($this->__tokens[$position][0] == T_COMMENT || $this->__tokens[$position] == ',' || $this->__tokens[$position] == ')') {
325                                        $strings[] = $string;
326                                }
327                        } else if ($this->__tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
328                                $strings[] = $this->__formatString($this->__tokens[$position][1]);
329                        }
330                        $position++;
331                }
332                return $strings;
333        }
334       
335/**
336 * Build the translate template file contents out of obtained strings
337 *
338 * @return void
339 * @access private
340 */
341        function __buildFiles() {
342                foreach ($this->__strings as $domain => $strings) {
343                        foreach ($strings as $string => $files) {
344                                $occurrences = array();
345                                foreach ($files as $file => $lines) {
346                                        $occurrences[] = $file . ':' . implode(';', $lines);
347                                }
348                                $occurrences = implode("\n#: ", $occurrences);
349                                $header = '#: ' . str_replace($this->__paths, '', $occurrences) . "\n";
350
351                                if (strpos($string, "\0") === false) {
352                                        $sentence = "msgid \"{$string}\"\n";
353                                        $sentence .= "msgstr \"\"\n\n";
354                                } else {
355                                        list($singular, $plural) = explode("\0", $string);
356                                        $sentence = "msgid \"{$singular}\"\n";
357                                        $sentence .= "msgid_plural \"{$plural}\"\n";
358                                        $sentence .= "msgstr[0] \"\"\n";
359                                        $sentence .= "msgstr[1] \"\"\n\n";
360                                }
361
362                                $this->__store($domain, $header, $sentence);
363                                if ($domain != 'default' && $this->__merge) {
364                                        $this->__store('default', $header, $sentence);
365                                }
366                        }
367                }
368        }
369
370/**
371 * Prepare a file to be stored
372 *
373 * @return void
374 * @access private
375 */
376        function __store($domain, $header, $sentence) {
377                if (!isset($this->__storage[$domain])) {
378                        $this->__storage[$domain] = array();
379                }
380                if (!isset($this->__storage[$domain][$sentence])) {
381                        $this->__storage[$domain][$sentence] = $header;
382                } else {
383                        $this->__storage[$domain][$sentence] .= $header;
384                }
385        }
386
387/**
388 * Write the files that need to be stored
389 *
390 * @return void
391 * @access private
392 */
393        function __writeFiles() {
394                $overwriteAll = false;
395                foreach ($this->__storage as $domain => $sentences) {
396                        $output = $this->__writeHeader();
397                        foreach ($sentences as $sentence => $header) {
398                                $output .= $header . $sentence;
399                        }
400
401                        $filename = $domain . '.pot';
402                        $File = new File($this->__output . $filename);
403                        $response = '';
404                        while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') {
405                                $this->out();
406                                $response = $this->in(sprintf(__('Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', true), $filename), array('y', 'n', 'a'), 'y');
407                                if (strtoupper($response) === 'N') {
408                                        $response = '';
409                                        while ($response == '') {
410                                                $response = $this->in(sprintf(__("What would you like to name this file?\nExample: %s", true), 'new_' . $filename), null, 'new_' . $filename);
411                                                $File = new File($this->__output . $response);
412                                                $filename = $response;
413                                        }
414                                } elseif (strtoupper($response) === 'A') {
415                                        $overwriteAll = true;
416                                }
417                        }
418                        $File->write($output);
419                        $File->close();
420                }
421        }
422
423/**
424 * Build the translation template header
425 *
426 * @return string Translation template header
427 * @access private
428 */
429        function __writeHeader() {
430                $output  = "# LANGUAGE translation of CakePHP Application\n";
431                $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
432                $output .= "#\n";
433                $output .= "#, fuzzy\n";
434                $output .= "msgid \"\"\n";
435                $output .= "msgstr \"\"\n";
436                $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
437                $output .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
438                $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
439                $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
440                $output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
441                $output .= "\"MIME-Version: 1.0\\n\"\n";
442                $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
443                $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
444                $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
445                return $output;
446        }
447
448/**
449 * Format a string to be added as a translateable string
450 *
451 * @param string $string String to format
452 * @return string Formatted string
453 * @access private
454 */
455        function __formatString($string) {
456                $quote = substr($string, 0, 1);
457                $string = substr($string, 1, -1);
458                if ($quote == '"') {
459                        $string = stripcslashes($string);
460                } else {
461                        $string = strtr($string, array("\\'" => "'", "\\\\" => "\\"));
462                }
463                $string = str_replace("\r\n", "\n", $string);
464                return addcslashes($string, "\0..\37\\\"");
465        }
466
467/**
468 * Indicate an invalid marker on a processed file
469 *
470 * @param string $file File where invalid marker resides
471 * @param integer $line Line number
472 * @param string $marker Marker found
473 * @param integer $count Count
474 * @return void
475 * @access private
476 */
477        function __markerError($file, $line, $marker, $count) {
478                $this->out(sprintf(__("Invalid marker content in %s:%s\n* %s(", true), $file, $line, $marker), true);
479                $count += 2;
480                $tokenCount = count($this->__tokens);
481                $parenthesis = 1;
482
483                while ((($tokenCount - $count) > 0) && $parenthesis) {
484                        if (is_array($this->__tokens[$count])) {
485                                $this->out($this->__tokens[$count][1], false);
486                        } else {
487                                $this->out($this->__tokens[$count], false);
488                                if ($this->__tokens[$count] == '(') {
489                                        $parenthesis++;
490                                }
491
492                                if ($this->__tokens[$count] == ')') {
493                                        $parenthesis--;
494                                }
495                        }
496                        $count++;
497                }
498                $this->out("\n", true);
499        }
500
501/**
502 * Search files that may contain translateable strings
503 *
504 * @return void
505 * @access private
506 */
507        function __searchFiles() {
508                foreach ($this->__paths as $path) {
509                        $Folder = new Folder($path);
510                        $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true);
511                        $this->__files = array_merge($this->__files, $files);
512                }
513        }
514}
Note: See TracBrowser for help on using the repository browser.