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