ExtractTask.php 24 KB

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