ExtractTask.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  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. ]);
  311. return $parser;
  312. }
  313. /**
  314. * Extract tokens out of all files to be processed
  315. *
  316. * @return void
  317. */
  318. protected function _extractTokens()
  319. {
  320. foreach ($this->_files as $file) {
  321. $this->_file = $file;
  322. $this->out(sprintf('Processing %s...', $file), 1, Shell::VERBOSE);
  323. $code = file_get_contents($file);
  324. $allTokens = token_get_all($code);
  325. $this->_tokens = [];
  326. foreach ($allTokens as $token) {
  327. if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) {
  328. $this->_tokens[] = $token;
  329. }
  330. }
  331. unset($allTokens);
  332. $this->_parse('__', ['singular']);
  333. $this->_parse('__n', ['singular', 'plural']);
  334. $this->_parse('__d', ['domain', 'singular']);
  335. $this->_parse('__dn', ['domain', 'singular', 'plural']);
  336. $this->_parse('__x', ['context', 'singular']);
  337. $this->_parse('__xn', ['context', 'singular', 'plural']);
  338. $this->_parse('__dx', ['domain', 'context', 'singular']);
  339. $this->_parse('__dxn', ['domain', 'context', 'singular', 'plural']);
  340. }
  341. }
  342. /**
  343. * Parse tokens
  344. *
  345. * @param string $functionName Function name that indicates translatable string (e.g: '__')
  346. * @param array $map Array containing what variables it will find (e.g: domain, singular, plural)
  347. * @return void
  348. */
  349. protected function _parse($functionName, $map)
  350. {
  351. $count = 0;
  352. $tokenCount = count($this->_tokens);
  353. while (($tokenCount - $count) > 1) {
  354. $countToken = $this->_tokens[$count];
  355. $firstParenthesis = $this->_tokens[$count + 1];
  356. if (!is_array($countToken)) {
  357. $count++;
  358. continue;
  359. }
  360. list($type, $string, $line) = $countToken;
  361. if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) {
  362. $position = $count;
  363. $depth = 0;
  364. while (!$depth) {
  365. if ($this->_tokens[$position] === '(') {
  366. $depth++;
  367. } elseif ($this->_tokens[$position] === ')') {
  368. $depth--;
  369. }
  370. $position++;
  371. }
  372. $mapCount = count($map);
  373. $strings = $this->_getStrings($position, $mapCount);
  374. if ($mapCount === count($strings)) {
  375. extract(array_combine($map, $strings));
  376. $domain = isset($domain) ? $domain : 'default';
  377. $details = [
  378. 'file' => $this->_file,
  379. 'line' => $line,
  380. ];
  381. if (isset($plural)) {
  382. $details['msgid_plural'] = $plural;
  383. }
  384. if (isset($context)) {
  385. $details['msgctxt'] = $context;
  386. }
  387. $this->_addTranslation($domain, $singular, $details);
  388. } elseif (strpos($this->_file, CAKE_CORE_INCLUDE_PATH) === false) {
  389. $this->_markerError($this->_file, $line, $functionName, $count);
  390. }
  391. }
  392. $count++;
  393. }
  394. }
  395. /**
  396. * Build the translate template file contents out of obtained strings
  397. *
  398. * @return void
  399. */
  400. protected function _buildFiles()
  401. {
  402. $paths = $this->_paths;
  403. $paths[] = realpath(APP) . DS;
  404. foreach ($this->_translations as $domain => $translations) {
  405. foreach ($translations as $msgid => $contexts) {
  406. foreach ($contexts as $context => $details) {
  407. $plural = $details['msgid_plural'];
  408. $files = $details['references'];
  409. $occurrences = [];
  410. foreach ($files as $file => $lines) {
  411. $lines = array_unique($lines);
  412. $occurrences[] = $file . ':' . implode(';', $lines);
  413. }
  414. $occurrences = implode("\n#: ", $occurrences);
  415. $header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n";
  416. $sentence = '';
  417. if ($context !== "") {
  418. $sentence .= "msgctxt \"{$context}\"\n";
  419. }
  420. if ($plural === false) {
  421. $sentence .= "msgid \"{$msgid}\"\n";
  422. $sentence .= "msgstr \"\"\n\n";
  423. } else {
  424. $sentence .= "msgid \"{$msgid}\"\n";
  425. $sentence .= "msgid_plural \"{$plural}\"\n";
  426. $sentence .= "msgstr[0] \"\"\n";
  427. $sentence .= "msgstr[1] \"\"\n\n";
  428. }
  429. $this->_store($domain, $header, $sentence);
  430. if ($domain !== 'default' && $this->_merge) {
  431. $this->_store('default', $header, $sentence);
  432. }
  433. }
  434. }
  435. }
  436. }
  437. /**
  438. * Prepare a file to be stored
  439. *
  440. * @param string $domain The domain
  441. * @param string $header The header content.
  442. * @param string $sentence The sentence to store.
  443. * @return void
  444. */
  445. protected function _store($domain, $header, $sentence)
  446. {
  447. if (!isset($this->_storage[$domain])) {
  448. $this->_storage[$domain] = [];
  449. }
  450. if (!isset($this->_storage[$domain][$sentence])) {
  451. $this->_storage[$domain][$sentence] = $header;
  452. } else {
  453. $this->_storage[$domain][$sentence] .= $header;
  454. }
  455. }
  456. /**
  457. * Write the files that need to be stored
  458. *
  459. * @return void
  460. */
  461. protected function _writeFiles()
  462. {
  463. $overwriteAll = false;
  464. if (!empty($this->params['overwrite'])) {
  465. $overwriteAll = true;
  466. }
  467. foreach ($this->_storage as $domain => $sentences) {
  468. $output = $this->_writeHeader();
  469. foreach ($sentences as $sentence => $header) {
  470. $output .= $header . $sentence;
  471. }
  472. $filename = $domain . '.pot';
  473. $File = new File($this->_output . $filename);
  474. $response = '';
  475. while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') {
  476. $this->out();
  477. $response = $this->in(
  478. sprintf('Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename),
  479. ['y', 'n', 'a'],
  480. 'y'
  481. );
  482. if (strtoupper($response) === 'N') {
  483. $response = '';
  484. while (!$response) {
  485. $response = $this->in("What would you like to name this file?", null, 'new_' . $filename);
  486. $File = new File($this->_output . $response);
  487. $filename = $response;
  488. }
  489. } elseif (strtoupper($response) === 'A') {
  490. $overwriteAll = true;
  491. }
  492. }
  493. $File->write($output);
  494. $File->close();
  495. }
  496. }
  497. /**
  498. * Build the translation template header
  499. *
  500. * @return string Translation template header
  501. */
  502. protected function _writeHeader()
  503. {
  504. $output = "# LANGUAGE translation of CakePHP Application\n";
  505. $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
  506. $output .= "#\n";
  507. $output .= "#, fuzzy\n";
  508. $output .= "msgid \"\"\n";
  509. $output .= "msgstr \"\"\n";
  510. $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
  511. $output .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
  512. $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
  513. $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
  514. $output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
  515. $output .= "\"MIME-Version: 1.0\\n\"\n";
  516. $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
  517. $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
  518. $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
  519. return $output;
  520. }
  521. /**
  522. * Get the strings from the position forward
  523. *
  524. * @param int $position Actual position on tokens array
  525. * @param int $target Number of strings to extract
  526. * @return array Strings extracted
  527. */
  528. protected function _getStrings(&$position, $target)
  529. {
  530. $strings = [];
  531. $count = count($strings);
  532. while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) {
  533. $count = count($strings);
  534. if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') {
  535. $string = '';
  536. while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') {
  537. if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
  538. $string .= $this->_formatString($this->_tokens[$position][1]);
  539. }
  540. $position++;
  541. }
  542. $strings[] = $string;
  543. } elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
  544. $strings[] = $this->_formatString($this->_tokens[$position][1]);
  545. } elseif ($this->_tokens[$position][0] == T_LNUMBER) {
  546. $strings[] = $this->_tokens[$position][1];
  547. }
  548. $position++;
  549. }
  550. return $strings;
  551. }
  552. /**
  553. * Format a string to be added as a translatable string
  554. *
  555. * @param string $string String to format
  556. * @return string Formatted string
  557. */
  558. protected function _formatString($string)
  559. {
  560. $quote = substr($string, 0, 1);
  561. $string = substr($string, 1, -1);
  562. if ($quote === '"') {
  563. $string = stripcslashes($string);
  564. } else {
  565. $string = strtr($string, ["\\'" => "'", "\\\\" => "\\"]);
  566. }
  567. $string = str_replace("\r\n", "\n", $string);
  568. return addcslashes($string, "\0..\37\\\"");
  569. }
  570. /**
  571. * Indicate an invalid marker on a processed file
  572. *
  573. * @param string $file File where invalid marker resides
  574. * @param int $line Line number
  575. * @param string $marker Marker found
  576. * @param int $count Count
  577. * @return void
  578. */
  579. protected function _markerError($file, $line, $marker, $count)
  580. {
  581. $this->err(sprintf("Invalid marker content in %s:%s\n* %s(", $file, $line, $marker));
  582. $count += 2;
  583. $tokenCount = count($this->_tokens);
  584. $parenthesis = 1;
  585. while ((($tokenCount - $count) > 0) && $parenthesis) {
  586. if (is_array($this->_tokens[$count])) {
  587. $this->err($this->_tokens[$count][1], false);
  588. } else {
  589. $this->err($this->_tokens[$count], false);
  590. if ($this->_tokens[$count] === '(') {
  591. $parenthesis++;
  592. }
  593. if ($this->_tokens[$count] === ')') {
  594. $parenthesis--;
  595. }
  596. }
  597. $count++;
  598. }
  599. $this->err("\n", true);
  600. }
  601. /**
  602. * Search files that may contain translatable strings
  603. *
  604. * @return void
  605. */
  606. protected function _searchFiles()
  607. {
  608. $pattern = false;
  609. if (!empty($this->_exclude)) {
  610. $exclude = [];
  611. foreach ($this->_exclude as $e) {
  612. if (DS !== '\\' && $e[0] !== DS) {
  613. $e = DS . $e;
  614. }
  615. $exclude[] = preg_quote($e, '/');
  616. }
  617. $pattern = '/' . implode('|', $exclude) . '/';
  618. }
  619. foreach ($this->_paths as $path) {
  620. $Folder = new Folder($path);
  621. $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true);
  622. if (!empty($pattern)) {
  623. foreach ($files as $i => $file) {
  624. if (preg_match($pattern, $file)) {
  625. unset($files[$i]);
  626. }
  627. }
  628. $files = array_values($files);
  629. }
  630. $this->_files = array_merge($this->_files, $files);
  631. }
  632. }
  633. /**
  634. * Returns whether this execution is meant to extract string only from directories in folder represented by the
  635. * APP constant, i.e. this task is extracting strings from same application.
  636. *
  637. * @return bool
  638. */
  639. protected function _isExtractingApp()
  640. {
  641. return $this->_paths === [APP];
  642. }
  643. /**
  644. * Checks whether or not a given path is usable for writing.
  645. *
  646. * @param string $path Path to folder
  647. * @return bool true if it exists and is writable, false otherwise
  648. */
  649. protected function _isPathUsable($path)
  650. {
  651. return is_dir($path) && is_writable($path);
  652. }
  653. }