PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/cake/console/shells/tasks/extract.php

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