ExtractTask.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 1.2.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Shell\Task;
  16. use Cake\Console\Shell;
  17. use Cake\Core\App;
  18. use Cake\Core\Plugin;
  19. use Cake\Filesystem\File;
  20. use Cake\Filesystem\Folder;
  21. use Cake\Utility\Inflector;
  22. /**
  23. * Language string extractor
  24. */
  25. class ExtractTask extends Shell
  26. {
  27. /**
  28. * Paths to use when looking for strings
  29. *
  30. * @var array
  31. */
  32. protected $_paths = [];
  33. /**
  34. * Files from where to extract
  35. *
  36. * @var array
  37. */
  38. protected $_files = [];
  39. /**
  40. * Merge all domain strings into the default.pot file
  41. *
  42. * @var bool
  43. */
  44. protected $_merge = false;
  45. /**
  46. * Current file being processed
  47. *
  48. * @var string|null
  49. */
  50. protected $_file = null;
  51. /**
  52. * Contains all content waiting to be write
  53. *
  54. * @var array
  55. */
  56. protected $_storage = [];
  57. /**
  58. * Extracted tokens
  59. *
  60. * @var array
  61. */
  62. protected $_tokens = [];
  63. /**
  64. * Extracted strings indexed by domain.
  65. *
  66. * @var array
  67. */
  68. protected $_translations = [];
  69. /**
  70. * Destination path
  71. *
  72. * @var string|null
  73. */
  74. protected $_output = null;
  75. /**
  76. * An array of directories to exclude.
  77. *
  78. * @var array
  79. */
  80. protected $_exclude = [];
  81. /**
  82. * Holds the validation string domain to use for validation messages when extracting
  83. *
  84. * @var bool
  85. */
  86. protected $_validationDomain = 'default';
  87. /**
  88. * Holds whether this call should extract the CakePHP Lib messages
  89. *
  90. * @var bool
  91. */
  92. protected $_extractCore = false;
  93. /**
  94. * No welcome message.
  95. *
  96. * @return void
  97. */
  98. protected function _welcome()
  99. {
  100. }
  101. /**
  102. * Method to interact with the User and get path selections.
  103. *
  104. * @return void
  105. */
  106. protected function _getPaths()
  107. {
  108. $defaultPath = APP;
  109. while (true) {
  110. $currentPaths = count($this->_paths) > 0 ? $this->_paths : ['None'];
  111. $message = sprintf(
  112. "Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one",
  113. implode(', ', $currentPaths)
  114. );
  115. $response = $this->in($message, null, $defaultPath);
  116. if (strtoupper($response) === 'Q') {
  117. $this->err('Extract Aborted');
  118. $this->_stop();
  119. return;
  120. }
  121. if (strtoupper($response) === 'D' && count($this->_paths)) {
  122. $this->out();
  123. return;
  124. }
  125. if (strtoupper($response) === 'D') {
  126. $this->warn('No directories selected. Please choose a directory.');
  127. } elseif (is_dir($response)) {
  128. $this->_paths[] = $response;
  129. $defaultPath = 'D';
  130. } else {
  131. $this->err('The directory path you supplied was not found. Please try again.');
  132. }
  133. $this->out();
  134. }
  135. }
  136. /**
  137. * Execution method always used for tasks
  138. *
  139. * @return void
  140. */
  141. public function main()
  142. {
  143. if (!empty($this->params['exclude'])) {
  144. $this->_exclude = explode(',', $this->params['exclude']);
  145. }
  146. if (isset($this->params['files']) && !is_array($this->params['files'])) {
  147. $this->_files = explode(',', $this->params['files']);
  148. }
  149. if (isset($this->params['paths'])) {
  150. $this->_paths = explode(',', $this->params['paths']);
  151. } elseif (isset($this->params['plugin'])) {
  152. $plugin = Inflector::camelize($this->params['plugin']);
  153. if (!Plugin::loaded($plugin)) {
  154. Plugin::load($plugin);
  155. }
  156. $this->_paths = [Plugin::classPath($plugin)];
  157. $this->params['plugin'] = $plugin;
  158. } else {
  159. $this->_getPaths();
  160. }
  161. if (isset($this->params['extract-core'])) {
  162. $this->_extractCore = !(strtolower($this->params['extract-core']) === 'no');
  163. } else {
  164. $response = $this->in('Would you like to extract the messages from the CakePHP core?', ['y', 'n'], 'n');
  165. $this->_extractCore = strtolower($response) === 'y';
  166. }
  167. if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) {
  168. $this->_exclude = array_merge($this->_exclude, App::path('Plugin'));
  169. }
  170. if (!empty($this->params['validation-domain'])) {
  171. $this->_validationDomain = $this->params['validation-domain'];
  172. }
  173. if ($this->_extractCore) {
  174. $this->_paths[] = CAKE;
  175. }
  176. if (isset($this->params['output'])) {
  177. $this->_output = $this->params['output'];
  178. } elseif (isset($this->params['plugin'])) {
  179. $this->_output = $this->_paths[0] . 'Locale';
  180. } else {
  181. $message = "What is the path you would like to output?\n[Q]uit";
  182. while (true) {
  183. $response = $this->in($message, null, rtrim($this->_paths[0], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Locale');
  184. if (strtoupper($response) === 'Q') {
  185. $this->err('Extract Aborted');
  186. $this->_stop();
  187. return;
  188. }
  189. if ($this->_isPathUsable($response)) {
  190. $this->_output = $response . DIRECTORY_SEPARATOR;
  191. break;
  192. }
  193. $this->err('');
  194. $this->err(
  195. '<error>The directory path you supplied was ' .
  196. 'not found. Please try again.</error>'
  197. );
  198. $this->out();
  199. }
  200. }
  201. if (isset($this->params['merge'])) {
  202. $this->_merge = !(strtolower($this->params['merge']) === 'no');
  203. } else {
  204. $this->out();
  205. $response = $this->in('Would you like to merge all domain strings into the default.pot file?', ['y', 'n'], 'n');
  206. $this->_merge = strtolower($response) === 'y';
  207. }
  208. if (empty($this->_files)) {
  209. $this->_searchFiles();
  210. }
  211. $this->_output = rtrim($this->_output, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
  212. if (!$this->_isPathUsable($this->_output)) {
  213. $this->err(sprintf('The output directory %s was not found or writable.', $this->_output));
  214. $this->_stop();
  215. return;
  216. }
  217. $this->_extract();
  218. }
  219. /**
  220. * Add a translation to the internal translations property
  221. *
  222. * Takes care of duplicate translations
  223. *
  224. * @param string $domain The domain
  225. * @param string $msgid The message string
  226. * @param array $details Context and plural form if any, file and line references
  227. * @return void
  228. */
  229. protected function _addTranslation($domain, $msgid, $details = [])
  230. {
  231. $context = isset($details['msgctxt']) ? $details['msgctxt'] : "";
  232. if (empty($this->_translations[$domain][$msgid][$context])) {
  233. $this->_translations[$domain][$msgid][$context] = [
  234. 'msgid_plural' => false
  235. ];
  236. }
  237. if (isset($details['msgid_plural'])) {
  238. $this->_translations[$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural'];
  239. }
  240. if (isset($details['file'])) {
  241. $line = isset($details['line']) ? $details['line'] : 0;
  242. $this->_translations[$domain][$msgid][$context]['references'][$details['file']][] = $line;
  243. }
  244. }
  245. /**
  246. * Extract text
  247. *
  248. * @return void
  249. */
  250. protected function _extract()
  251. {
  252. $this->out();
  253. $this->out();
  254. $this->out('Extracting...');
  255. $this->hr();
  256. $this->out('Paths:');
  257. foreach ($this->_paths as $path) {
  258. $this->out(' ' . $path);
  259. }
  260. $this->out('Output Directory: ' . $this->_output);
  261. $this->hr();
  262. $this->_extractTokens();
  263. $this->_buildFiles();
  264. $this->_writeFiles();
  265. $this->_paths = $this->_files = $this->_storage = [];
  266. $this->_translations = $this->_tokens = [];
  267. $this->out();
  268. $this->out('Done.');
  269. }
  270. /**
  271. * Gets the option parser instance and configures it.
  272. *
  273. * @return \Cake\Console\ConsoleOptionParser
  274. */
  275. public function getOptionParser()
  276. {
  277. $parser = parent::getOptionParser();
  278. $parser->description(
  279. 'CakePHP Language String Extraction:'
  280. )->addOption('app', [
  281. 'help' => 'Directory where your application is located.'
  282. ])->addOption('paths', [
  283. 'help' => 'Comma separated list of paths.'
  284. ])->addOption('merge', [
  285. 'help' => 'Merge all domain strings into the default.po file.',
  286. 'choices' => ['yes', 'no']
  287. ])->addOption('output', [
  288. 'help' => 'Full path to output directory.'
  289. ])->addOption('files', [
  290. 'help' => 'Comma separated list of files.'
  291. ])->addOption('exclude-plugins', [
  292. 'boolean' => true,
  293. 'default' => true,
  294. 'help' => 'Ignores all files in plugins if this command is run inside from the same app directory.'
  295. ])->addOption('plugin', [
  296. 'help' => 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.'
  297. ])->addOption('ignore-model-validation', [
  298. 'boolean' => true,
  299. 'default' => false,
  300. 'help' => 'Ignores validation messages in the $validate property.' .
  301. ' If this flag is not set and the command is run from the same app directory,' .
  302. ' all messages in model validation rules will be extracted as tokens.'
  303. ])->addOption('validation-domain', [
  304. 'help' => 'If set to a value, the localization domain to be used for model validation messages.'
  305. ])->addOption('exclude', [
  306. 'help' => 'Comma separated list of directories to exclude.' .
  307. ' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors'
  308. ])->addOption('overwrite', [
  309. 'boolean' => true,
  310. 'default' => false,
  311. 'help' => 'Always overwrite existing .pot files.'
  312. ])->addOption('extract-core', [
  313. 'help' => 'Extract messages from the CakePHP core libs.',
  314. 'choices' => ['yes', 'no']
  315. ])->addOption('no-location', [
  316. 'boolean' => true,
  317. 'default' => false,
  318. 'help' => 'Do not write file locations for each extracted message.',
  319. ]);
  320. return $parser;
  321. }
  322. /**
  323. * Extract tokens out of all files to be processed
  324. *
  325. * @return void
  326. */
  327. protected function _extractTokens()
  328. {
  329. $progress = $this->helper('progress');
  330. $progress->init(['total' => count($this->_files)]);
  331. $isVerbose = $this->param('verbose');
  332. foreach ($this->_files as $file) {
  333. $this->_file = $file;
  334. if ($isVerbose) {
  335. $this->out(sprintf('Processing %s...', $file), 1, Shell::VERBOSE);
  336. }
  337. $code = file_get_contents($file);
  338. $allTokens = token_get_all($code);
  339. $this->_tokens = [];
  340. foreach ($allTokens as $token) {
  341. if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) {
  342. $this->_tokens[] = $token;
  343. }
  344. }
  345. unset($allTokens);
  346. $this->_parse('__', ['singular']);
  347. $this->_parse('__n', ['singular', 'plural']);
  348. $this->_parse('__d', ['domain', 'singular']);
  349. $this->_parse('__dn', ['domain', 'singular', 'plural']);
  350. $this->_parse('__x', ['context', 'singular']);
  351. $this->_parse('__xn', ['context', 'singular', 'plural']);
  352. $this->_parse('__dx', ['domain', 'context', 'singular']);
  353. $this->_parse('__dxn', ['domain', 'context', 'singular', 'plural']);
  354. if (!$isVerbose) {
  355. $progress->increment(1);
  356. $progress->draw();
  357. }
  358. }
  359. }
  360. /**
  361. * Parse tokens
  362. *
  363. * @param string $functionName Function name that indicates translatable string (e.g: '__')
  364. * @param array $map Array containing what variables it will find (e.g: domain, singular, plural)
  365. * @return void
  366. */
  367. protected function _parse($functionName, $map)
  368. {
  369. $count = 0;
  370. $tokenCount = count($this->_tokens);
  371. while (($tokenCount - $count) > 1) {
  372. $countToken = $this->_tokens[$count];
  373. $firstParenthesis = $this->_tokens[$count + 1];
  374. if (!is_array($countToken)) {
  375. $count++;
  376. continue;
  377. }
  378. list($type, $string, $line) = $countToken;
  379. if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) {
  380. $position = $count;
  381. $depth = 0;
  382. while (!$depth) {
  383. if ($this->_tokens[$position] === '(') {
  384. $depth++;
  385. } elseif ($this->_tokens[$position] === ')') {
  386. $depth--;
  387. }
  388. $position++;
  389. }
  390. $mapCount = count($map);
  391. $strings = $this->_getStrings($position, $mapCount);
  392. if ($mapCount === count($strings)) {
  393. extract(array_combine($map, $strings));
  394. $domain = isset($domain) ? $domain : 'default';
  395. $details = [
  396. 'file' => $this->_file,
  397. 'line' => $line,
  398. ];
  399. if (isset($plural)) {
  400. $details['msgid_plural'] = $plural;
  401. }
  402. if (isset($context)) {
  403. $details['msgctxt'] = $context;
  404. }
  405. $this->_addTranslation($domain, $singular, $details);
  406. } elseif (strpos($this->_file, CAKE_CORE_INCLUDE_PATH) === false) {
  407. $this->_markerError($this->_file, $line, $functionName, $count);
  408. }
  409. }
  410. $count++;
  411. }
  412. }
  413. /**
  414. * Build the translate template file contents out of obtained strings
  415. *
  416. * @return void
  417. */
  418. protected function _buildFiles()
  419. {
  420. $paths = $this->_paths;
  421. $paths[] = realpath(APP) . DIRECTORY_SEPARATOR;
  422. usort($paths, function ($a, $b) {
  423. return strlen($a) - strlen($b);
  424. });
  425. foreach ($this->_translations as $domain => $translations) {
  426. foreach ($translations as $msgid => $contexts) {
  427. foreach ($contexts as $context => $details) {
  428. $plural = $details['msgid_plural'];
  429. $files = $details['references'];
  430. $occurrences = [];
  431. foreach ($files as $file => $lines) {
  432. $lines = array_unique($lines);
  433. $occurrences[] = $file . ':' . implode(';', $lines);
  434. }
  435. $occurrences = implode("\n#: ", $occurrences);
  436. $header = "";
  437. if (!$this->param('no-location')) {
  438. $header = '#: ' . str_replace(DIRECTORY_SEPARATOR, '/', str_replace($paths, '', $occurrences)) . "\n";
  439. }
  440. $sentence = '';
  441. if ($context !== "") {
  442. $sentence .= "msgctxt \"{$context}\"\n";
  443. }
  444. if ($plural === false) {
  445. $sentence .= "msgid \"{$msgid}\"\n";
  446. $sentence .= "msgstr \"\"\n\n";
  447. } else {
  448. $sentence .= "msgid \"{$msgid}\"\n";
  449. $sentence .= "msgid_plural \"{$plural}\"\n";
  450. $sentence .= "msgstr[0] \"\"\n";
  451. $sentence .= "msgstr[1] \"\"\n\n";
  452. }
  453. if ($domain !== 'default' && $this->_merge) {
  454. $this->_store('default', $header, $sentence);
  455. } else {
  456. $this->_store($domain, $header, $sentence);
  457. }
  458. }
  459. }
  460. }
  461. }
  462. /**
  463. * Prepare a file to be stored
  464. *
  465. * @param string $domain The domain
  466. * @param string $header The header content.
  467. * @param string $sentence The sentence to store.
  468. * @return void
  469. */
  470. protected function _store($domain, $header, $sentence)
  471. {
  472. if (!isset($this->_storage[$domain])) {
  473. $this->_storage[$domain] = [];
  474. }
  475. if (!isset($this->_storage[$domain][$sentence])) {
  476. $this->_storage[$domain][$sentence] = $header;
  477. } else {
  478. $this->_storage[$domain][$sentence] .= $header;
  479. }
  480. }
  481. /**
  482. * Write the files that need to be stored
  483. *
  484. * @return void
  485. */
  486. protected function _writeFiles()
  487. {
  488. $overwriteAll = false;
  489. if (!empty($this->params['overwrite'])) {
  490. $overwriteAll = true;
  491. }
  492. foreach ($this->_storage as $domain => $sentences) {
  493. $output = $this->_writeHeader();
  494. foreach ($sentences as $sentence => $header) {
  495. $output .= $header . $sentence;
  496. }
  497. // Remove vendor prefix if present.
  498. $slashPosition = strpos($domain, '/');
  499. if ($slashPosition !== false) {
  500. $domain = substr($domain, $slashPosition + 1);
  501. }
  502. $filename = str_replace('/', '_', $domain) . '.pot';
  503. $File = new File($this->_output . $filename);
  504. $response = '';
  505. while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') {
  506. $this->out();
  507. $response = $this->in(
  508. sprintf('Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename),
  509. ['y', 'n', 'a'],
  510. 'y'
  511. );
  512. if (strtoupper($response) === 'N') {
  513. $response = '';
  514. while (!$response) {
  515. $response = $this->in("What would you like to name this file?", null, 'new_' . $filename);
  516. $File = new File($this->_output . $response);
  517. $filename = $response;
  518. }
  519. } elseif (strtoupper($response) === 'A') {
  520. $overwriteAll = true;
  521. }
  522. }
  523. $File->write($output);
  524. $File->close();
  525. }
  526. }
  527. /**
  528. * Build the translation template header
  529. *
  530. * @return string Translation template header
  531. */
  532. protected function _writeHeader()
  533. {
  534. $output = "# LANGUAGE translation of CakePHP Application\n";
  535. $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
  536. $output .= "#\n";
  537. $output .= "#, fuzzy\n";
  538. $output .= "msgid \"\"\n";
  539. $output .= "msgstr \"\"\n";
  540. $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
  541. $output .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
  542. $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
  543. $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
  544. $output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
  545. $output .= "\"MIME-Version: 1.0\\n\"\n";
  546. $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
  547. $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
  548. $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
  549. return $output;
  550. }
  551. /**
  552. * Get the strings from the position forward
  553. *
  554. * @param int $position Actual position on tokens array
  555. * @param int $target Number of strings to extract
  556. * @return array Strings extracted
  557. */
  558. protected function _getStrings(&$position, $target)
  559. {
  560. $strings = [];
  561. $count = count($strings);
  562. while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) {
  563. $count = count($strings);
  564. if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') {
  565. $string = '';
  566. while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') {
  567. if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
  568. $string .= $this->_formatString($this->_tokens[$position][1]);
  569. }
  570. $position++;
  571. }
  572. $strings[] = $string;
  573. } elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
  574. $strings[] = $this->_formatString($this->_tokens[$position][1]);
  575. } elseif ($this->_tokens[$position][0] == T_LNUMBER) {
  576. $strings[] = $this->_tokens[$position][1];
  577. }
  578. $position++;
  579. }
  580. return $strings;
  581. }
  582. /**
  583. * Format a string to be added as a translatable string
  584. *
  585. * @param string $string String to format
  586. * @return string Formatted string
  587. */
  588. protected function _formatString($string)
  589. {
  590. $quote = substr($string, 0, 1);
  591. $string = substr($string, 1, -1);
  592. if ($quote === '"') {
  593. $string = stripcslashes($string);
  594. } else {
  595. $string = strtr($string, ["\\'" => "'", "\\\\" => "\\"]);
  596. }
  597. $string = str_replace("\r\n", "\n", $string);
  598. return addcslashes($string, "\0..\37\\\"");
  599. }
  600. /**
  601. * Indicate an invalid marker on a processed file
  602. *
  603. * @param string $file File where invalid marker resides
  604. * @param int $line Line number
  605. * @param string $marker Marker found
  606. * @param int $count Count
  607. * @return void
  608. */
  609. protected function _markerError($file, $line, $marker, $count)
  610. {
  611. $this->err(sprintf("Invalid marker content in %s:%s\n* %s(", $file, $line, $marker));
  612. $count += 2;
  613. $tokenCount = count($this->_tokens);
  614. $parenthesis = 1;
  615. while ((($tokenCount - $count) > 0) && $parenthesis) {
  616. if (is_array($this->_tokens[$count])) {
  617. $this->err($this->_tokens[$count][1], false);
  618. } else {
  619. $this->err($this->_tokens[$count], false);
  620. if ($this->_tokens[$count] === '(') {
  621. $parenthesis++;
  622. }
  623. if ($this->_tokens[$count] === ')') {
  624. $parenthesis--;
  625. }
  626. }
  627. $count++;
  628. }
  629. $this->err("\n", true);
  630. }
  631. /**
  632. * Search files that may contain translatable strings
  633. *
  634. * @return void
  635. */
  636. protected function _searchFiles()
  637. {
  638. $pattern = false;
  639. if (!empty($this->_exclude)) {
  640. $exclude = [];
  641. foreach ($this->_exclude as $e) {
  642. if (DIRECTORY_SEPARATOR !== '\\' && $e[0] !== DIRECTORY_SEPARATOR) {
  643. $e = DIRECTORY_SEPARATOR . $e;
  644. }
  645. $exclude[] = preg_quote($e, '/');
  646. }
  647. $pattern = '/' . implode('|', $exclude) . '/';
  648. }
  649. foreach ($this->_paths as $path) {
  650. $path = realpath($path) . DIRECTORY_SEPARATOR;
  651. $Folder = new Folder($path);
  652. $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true);
  653. if (!empty($pattern)) {
  654. $files = preg_grep($pattern, $files, PREG_GREP_INVERT);
  655. $files = array_values($files);
  656. }
  657. $this->_files = array_merge($this->_files, $files);
  658. }
  659. $this->_files = array_unique($this->_files);
  660. }
  661. /**
  662. * Returns whether this execution is meant to extract string only from directories in folder represented by the
  663. * APP constant, i.e. this task is extracting strings from same application.
  664. *
  665. * @return bool
  666. */
  667. protected function _isExtractingApp()
  668. {
  669. return $this->_paths === [APP];
  670. }
  671. /**
  672. * Checks whether or not a given path is usable for writing.
  673. *
  674. * @param string $path Path to folder
  675. * @return bool true if it exists and is writable, false otherwise
  676. */
  677. protected function _isPathUsable($path)
  678. {
  679. if (!is_dir($path)) {
  680. mkdir($path, 0770, true);
  681. }
  682. return is_dir($path) && is_writable($path);
  683. }
  684. }