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