Fix package name for potx module
[project/potx.git] / potx.inc
1 <?php
2
3 /**
4 * @file
5 * Localization string extraction API.
6 *
7 * The main goal of the API is to let callers parse Drupal source code files
8 * and get strings found using the localization API.
9 *
10 * This include file implements the default string and file version
11 * storage as well as formatting of POT files for web download or
12 * file system level creation. The strings, versions and file contents
13 * are handled with global variables to reduce the possible memory overhead
14 * and API clutter of passing them around. Custom string and version saving
15 * functions can be implemented to use the functionality provided here as an
16 * API for Drupal code to translatable string conversion.
17 *
18 * For a module using potx as an extraction API, but providing more
19 * sophisticated functionality on top of it, look into the
20 * 'Localization server' module: http://drupal.org/project/l10n_server
21 */
22
23 /**
24 * The current Drupal major API verion.
25 *
26 * This should be the only difference between different branches of potx.inc
27 */
28 define('POTX_API_CURRENT', 7);
29
30 /**
31 * Save string to both installer and runtime collection.
32 */
33 define('POTX_STRING_BOTH', 0);
34
35 /**
36 * Save string to installer collection only.
37 */
38 define('POTX_STRING_INSTALLER', 1);
39
40 /**
41 * Save string to runtime collection only.
42 */
43 define('POTX_STRING_RUNTIME', 2);
44
45 /**
46 * Parse source files in Drupal 5.x format.
47 */
48 define('POTX_API_5', 5);
49
50 /**
51 * Parse source files in Drupal 6.x format.
52 *
53 * Changes since 5.x documented at http://drupal.org/node/114774
54 */
55 define('POTX_API_6', 6);
56
57 /**
58 * Parse source files in Drupal 7.x format.
59 *
60 * Changes since 6.x documented at http://drupal.org/node/224333
61 */
62 define('POTX_API_7', 7);
63
64 /**
65 * When no context is used. Makes it easy to look these up.
66 */
67 define('POTX_CONTEXT_NONE', NULL);
68
69 /**
70 * When there was a context identification error.
71 */
72 define('POTX_CONTEXT_ERROR', FALSE);
73
74 // === File lookup, parsing and output building ================================
75
76 /**
77 * Collect a list of source file names relevant for extraction.
78 *
79 * @param $path
80 * Where to start searching for files recursively.
81 * Provide non-empty path values with a trailing slash.
82 * @param $basename
83 * Allows the restriction of search to a specific basename
84 * (ie. to collect files for a specific module).
85 * @param $api_version
86 * Drupal API version to work with.
87 */
88 function potx_parser_source_files($path = '', $basename = '*', $api_version = POTX_API_CURRENT) {
89 // It would be so nice to just use GLOB_BRACE, but it is not available on all
90 // operarting systems, so we are working around the missing functionality by
91 // going thrugh per extension.
92 $extensions = array('php', 'inc', 'module', 'engine', 'theme', 'install', 'info', 'profile');
93 if ($api_version > POTX_API_5) {
94 $extensions[] = 'js';
95 }
96 $files = array();
97 foreach ($extensions as $extension) {
98 $files_here = glob($path . $basename . '.' . $extension);
99 if (is_array($files_here)) {
100 $files = array_merge($files, $files_here);
101 }
102 if ($basename != '*') {
103 // Basename was specific, so look for things like basename.admin.inc as well.
104 // If the basnename was *, the above glob() already covered this case.
105 $files_here = glob($path . $basename . '.*.' . $extension);
106 if (is_array($files_here)) {
107 $files = array_merge($files, $files_here);
108 }
109 }
110 }
111
112 // Grab subdirectories.
113 $dirs = glob($path . '*', GLOB_ONLYDIR);
114 if (is_array($dirs)) {
115 foreach ($dirs as $dir) {
116 // Skip CVS, svn and git data as well as tests.
117 if (!preg_match("!(^|.+/)(CVS|\.svn|\.git|tests)$!", $dir)) {
118 $files = array_merge($files, potx_parser_source_files("$dir/", $basename));
119 }
120 }
121 }
122
123 foreach ($files as $id => $file_name) {
124 // Skip API and test files.
125 if (preg_match('!(\.api\.php|\.test)$!', $file_name)) {
126 unset($files[$id]);
127 }
128 }
129 return $files;
130 }
131
132 /**
133 * Process a file and save extracted information with the callbacks given.
134 *
135 * @param $file_path
136 * Comlete path to file to process.
137 * @param $display_name
138 * Display oriented file name for saved string data and error messages.
139 * @param $string_save_callback
140 * Callback function to use to save the collected strings.
141 * @param $file_version_callback
142 * Callback function to use to save collected version numbers.
143 * @param $api_version
144 * Drupal API version to work with.
145 */
146 function potx_parser_parse_file($file_path, $display_name = '', $string_save_callback = 'potx_parser_callback_save_string', $file_version_callback = 'potx_parser_callback_version', $api_version = POTX_API_CURRENT) {
147 global $_potx_tokens, $_potx_lookup;
148
149 $basename = basename($file_path);
150 $name_parts = pathinfo($basename);
151 $display_name = empty($display_name) ? $file_path : $display_name;
152
153 // Grab the CVS version number from the code if available.
154 // @todo: Drupal is moving to git, so this will not have much meaning then.
155 $code = file_get_contents($file_path);
156 potx_parser_get_version_number($code, $display_name, $file_version_callback);
157
158 if ($name_parts['extension'] == 'info') {
159 // .info files are not PHP code, no need to tokenize.
160 potx_parser_get_info_strings($file_path, $display_name, $string_save_callback, $api_version);
161 return;
162 }
163 elseif ($name_parts['extension'] == 'js') {
164 // JS files are not PHP code, no need to tokenize.
165 if ($api_version > POTX_API_5) {
166 potx_parser_get_js_strings($code, $display_name, $string_save_callback);
167 }
168 return;
169 }
170
171 // Extract raw PHP language tokens.
172 $raw_tokens = token_get_all($code);
173 unset($code);
174
175 // List of tokens to get an index of, so we can do quick lookups of use of
176 // these functions and don't need to look through the whole token array.
177 $tokens_to_index = array(
178 't',
179 'st',
180 '_locale_import_message',
181 'watchdog',
182 'format_plural',
183 $name_parts['filename'] . '_perm',
184 'node_perm',
185 $name_parts['filename'] . '_menu',
186 $name_parts['filename'] . '_menu_alter',
187 '_locale_get_predefined_list',
188 '_locale_get_iso639_list',
189 );
190
191 // Remove whitespace and possible HTML (the later in templates for example),
192 // count line numbers so we can include them in the output.
193 $_potx_tokens = array();
194 $_potx_lookup = array();
195 $token_number = 0;
196 $line_number = 1;
197 foreach ($raw_tokens as $token) {
198 if (!is_array($token) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) {
199 if (is_array($token)) {
200 $token[] = $line_number;
201 // Fill an array for finding token offsets quickly. We need the $t
202 // entries, as well as other functions invoked in the code that we
203 // look for.
204 if (($token[0] == T_STRING && in_array($token[1], $tokens_to_index)) || ($token[0] == T_VARIABLE && $token[1] == '$t')) {
205 if (!isset($_potx_lookup[$token[1]])) {
206 $_potx_lookup[$token[1]] = array();
207 }
208 $_potx_lookup[$token[1]][] = $token_number;
209 }
210 }
211 $_potx_tokens[] = $token;
212 $token_number++;
213 }
214
215 // Keep track of line numbers based on newlines in the token we just
216 // processed.
217 if (is_array($token)) {
218 $line_number += count(explode("\n", $token[1])) - 1;
219 }
220 else {
221 $line_number += count(explode("\n", $token)) - 1;
222 }
223 }
224 unset($raw_tokens);
225
226 // Drupal 7 onwards supports context on t(), st() and $t().
227 $use_context = ($api_version > POTX_API_6);
228 potx_parser_find_t_strings($display_name, $string_save_callback, 't', POTX_STRING_RUNTIME, $use_context);
229 potx_parser_find_t_strings($display_name, $string_save_callback, '$t', POTX_STRING_BOTH, $use_context);
230 potx_parser_find_t_strings($display_name, $string_save_callback, 'st', POTX_STRING_INSTALLER, $use_context);
231
232 // This does not support context even in Drupal 7.
233 potx_parser_find_t_strings($display_name, $string_save_callback, '_locale_import_message', POTX_STRING_BOTH);
234
235 if ($api_version > POTX_API_5) {
236 // Watchdog calls have both of their arguments translated from Drupal 6.x.
237 potx_parser_find_watchdog_strings($display_name, $string_save_callback);
238 }
239 else {
240 // Watchdog calls only have their first argument translated in Drupal 5.x
241 // and before.
242 potx_parser_find_t_strings($display_name, $string_save_callback, 'watchdog');
243 }
244
245 // Plurals need unique parsing.
246 potx_parser_find_format_plural_strings($display_name, $string_save_callback, $api_version);
247
248 if ($name_parts['extension'] == 'module') {
249 if ($api_version < POTX_API_7) {
250 potx_parser_find_perm_strings($display_name, $name_parts['filename'], $string_save_callback);
251 }
252 if ($api_version > POTX_API_5) {
253 potx_parser_find_menu_strings($display_name, $name_parts['filename'], $string_save_callback);
254 }
255 }
256
257 // Special handling of some Drupal core files.
258 if (($basename == 'locale.inc' && $api_version < POTX_API_7) || $basename == 'iso.inc') {
259 potx_parser_find_language_strings($display_name, $string_save_callback, $api_version);
260 }
261 elseif ($basename == 'locale.module') {
262 potx_parser_add_date_strings($display_name, $string_save_callback, $api_version);
263 }
264 elseif ($basename == 'common.inc') {
265 potx_parser_add_format_interval_strings($display_name, $string_save_callback, $api_version);
266 }
267 elseif ($basename == 'system.module') {
268 potx_parser_add_default_region_strings($display_name, $string_save_callback, $api_version);
269 }
270 elseif ($basename == 'user.module') {
271 // Save default user role names.
272 $string_save_callback('anonymous user', POTX_CONTEXT_NONE, $display_name);
273 $string_save_callback('authenticated user', POTX_CONTEXT_NONE, $display_name);
274 if ($api_version > POTX_API_6) {
275 // Administator role is included by default from Drupal 7.
276 $string_save_callback('administrator', POTX_CONTEXT_NONE, $display_name);
277 }
278 }
279
280 // Drop this data set that we don't need anymore.
281 $_potx_tokens = $_potx_lookup = array();
282 }
283
284 /**
285 * Build a complete .po file based on string data collected earlier.
286 *
287 * @param $string_mode
288 * Strings to generate files for: POTX_STRING_RUNTIME or POTX_STRING_INSTALLER.
289 * @param $string_save_callback
290 * Callback used to save strings previously.
291 * @param $file_version_callback
292 * Callback used to save versions previously.
293 * @param $header_callback
294 * Callback to invoke to get the POT header.
295 * @param $template_export_langcode
296 * Language code if the template should have language dependent content
297 * (like plural formulas and language name) included.
298 * @param $translation_export_langcode
299 * Language code if translations should also be exported.
300 * @param $api_version
301 * Drupal API version to work with.
302 */
303 function potx_parser_build_output($string_mode = POTX_STRING_RUNTIME, $string_save_callback = 'potx_parser_callback_save_string', $file_version_callback = 'potx_parser_callback_version', $header_callback = 'potx_parser_callback_header', $template_export_langcode = NULL, $translation_export_langcode = NULL, $api_version = POTX_API_CURRENT) {
304
305 // Initialize storage for .po file output.
306 $data = array(
307 'header' => $header_callback($template_export_langcode, $api_version),
308 'sources' => array(),
309 'strings' => '',
310 );
311
312 // Get strings and versions by reference.
313 $strings = $string_save_callback(NULL, NULL, NULL, 0, $string_mode);
314 $versions = $file_version_callback();
315
316 // We might not have any string recorded in this string mode.
317 if (!is_array($strings)) {
318 return;
319 }
320
321 foreach ($strings as $string => $string_info) {
322 foreach ($string_info as $context => $file_info) {
323 // Build a compact list of files this string occured in.
324 $occured = $file_list = array();
325 foreach ($file_info as $file => $lines) {
326 $occured[] = $file . ':' . join(';', $lines);
327 if (isset($versions[$file])) {
328 $file_list[] = $versions[$file];
329 }
330 }
331
332 // Mark duplicate strings (both translated in the app and in the installer).
333 $comment = join(' ', $occured);
334 if (strpos($comment, '(dup)') !== FALSE) {
335 $comment = '(duplicate) ' . str_replace('(dup)', '', $comment);
336 }
337 $output = "#: $comment\n";
338
339 if (strpos($string, "\0") !== FALSE) {
340 // Plural strings have a null byte delimited format.
341 list($singular, $plural) = explode("\0", $string);
342 if (!empty($context)) {
343 $output .= "msgctxt \"$context\"\n";
344 }
345 $output .= "msgid \"$singular\"\n";
346 $output .= "msgid_plural \"$plural\"\n";
347 if (isset($translation_export_langcode)) {
348 $output .= _potx_translation_export($translation_export_langcode, $singular, $plural, $api_version);
349 }
350 else {
351 $output .= "msgstr[0] \"\"\n";
352 $output .= "msgstr[1] \"\"\n";
353 }
354 }
355 else {
356 // Simple strings.
357 if (!empty($context)) {
358 $output .= "msgctxt \"$context\"\n";
359 }
360 $output .= "msgid \"$string\"\n";
361 if (isset($translation_export_langcode)) {
362 $output .= _potx_translation_export($translation_export_langcode, $string, NULL, $api_version);
363 }
364 else {
365 $output .= "msgstr \"\"\n";
366 }
367 }
368 $output .= "\n";
369
370 // Maintain a list of unique file names.
371 $data['sources'] = array_unique(array_merge($data['sources'], $file_list));
372 $data['strings'] .= $output;
373 }
374 }
375
376 // Build replacement for file listing.
377 if (count($data['sources']) > 1) {
378 $filelist = "Generated from files:\n# " . join("\n# ", $data['sources']);
379 }
380 elseif (count($data['sources']) == 1) {
381 $filelist = "Generated from file: " . join('', $data['sources']);
382 }
383 else {
384 $filelist = 'No version information was available in the source files.';
385 }
386 return str_replace('--VERSIONS--', $filelist, $data['header']) . $data['strings'];
387 }
388
389 /**
390 * Export translations with a specific language.
391 *
392 * @param $translation_export_langcode
393 * Language code if translations should also be exported.
394 * @param $string
395 * String or singular version if $plural was provided.
396 * @param $plural
397 * Plural version of singular string.
398 * @param $api_version
399 * Drupal API version to work with.
400 */
401 function _potx_translation_export($translation_export_langcode, $string, $plural = NULL, $api_version = POTX_API_CURRENT) {
402 include_once 'includes/locale.inc';
403
404 // Stip out slash escapes.
405 $string = stripcslashes($string);
406
407 // Column and table name changed between versions.
408 $language_column = $api_version > POTX_API_5 ? 'language' : 'locale';
409 $language_table = $api_version > POTX_API_5 ? 'languages' : 'locales_meta';
410
411 if (!isset($plural)) {
412 // Single string to look translation up for.
413 if ($translation = db_query("SELECT t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON t.lid = s.lid WHERE s.source = :source AND t.{$language_column} = :langcode", array(':source' => $string, ':langcode' => $translation_export_langcode))->fetchField()) {
414 return 'msgstr ' . _locale_export_string($translation);
415 }
416 return "msgstr \"\"\n";
417 }
418
419 else {
420 // String with plural variants. Fill up source string array first.
421 $plural = stripcslashes($plural);
422 $strings = array();
423 $number_of_plurals = db_query('SELECT plurals FROM {' . $language_table . "} WHERE {$language_column} = :langcode", array(':langcode' => $translation_export_langcode))->fetchField();
424 $plural_index = 0;
425 while ($plural_index < $number_of_plurals) {
426 if ($plural_index == 0) {
427 // Add the singular version.
428 $strings[] = $string;
429 }
430 elseif ($plural_index == 1) {
431 // Only add plural version if required.
432 $strings[] = $plural;
433 }
434 else {
435 // More plural versions only if required, with the lookup source
436 // string modified as imported into the database.
437 $strings[] = str_replace('@count', '@count[' . $plural_index . ']', $plural);
438 }
439 $plural_index++;
440 }
441
442 $output = '';
443 if (count($strings)) {
444 // Source string array was done, so export translations.
445 foreach ($strings as $index => $string) {
446 if ($translation = db_query("SELECT t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON t.lid = s.lid WHERE s.source = :source AND t.{$language_column} = :langcode", array(':source' => $string, ':langcode' => $translation_export_langcode))->fetchField()) {
447 $output .= 'msgstr[' . $index . '] ' . _locale_export_string(_locale_export_remove_plural($translation));
448 }
449 else {
450 $output .= 'msgstr[' . $index . "] \"\"\n";
451 }
452 }
453 }
454 else {
455 // No plural information was recorded, so export empty placeholders.
456 $output .= "msgstr[0] \"\"\n";
457 $output .= "msgstr[1] \"\"\n";
458 }
459 return $output;
460 }
461 }
462
463 // === Token based code tree parsers ===========================================
464
465 /**
466 * Detect all occurances of t()-like calls.
467 *
468 * These sequences are searched for:
469 * T_STRING("$function_name") + "(" + T_CONSTANT_ENCAPSED_STRING + ")"
470 * T_STRING("$function_name") + "(" + T_CONSTANT_ENCAPSED_STRING + ","
471 * and then an optional value for the replacements and an optional array
472 * for the options with an optional context key (for Drupal 7+).
473 *
474 * @param $display_name
475 * Name of file parsed.
476 * @param $string_save_callback
477 * Callback function used to save strings.
478 * @param function_name
479 * The name of the function to look for (could be 't', '$t', 'st'
480 * or any other t-like function). Drupal 7 only supports context on t(), st()
481 * and $t().
482 * @param $string_mode
483 * String mode to use: POTX_STRING_INSTALLER, POTX_STRING_RUNTIME or
484 * POTX_STRING_BOTH.
485 * @param $context_support
486 * Whether we should look for string context provided.
487 */
488 function potx_parser_find_t_strings($display_name, $string_save_callback, $function_name = 't', $string_mode = POTX_STRING_RUNTIME, $context_support = FALSE) {
489 global $_potx_tokens, $_potx_lookup;
490
491 // Lookup tokens by function name.
492 if (isset($_potx_lookup[$function_name])) {
493 foreach ($_potx_lookup[$function_name] as $ti) {
494 list($ctok, $par, $mid, $rig) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1], $_potx_tokens[$ti+2], $_potx_tokens[$ti+3]);
495 list($type, $string, $line) = $ctok;
496 if ($par == "(") {
497 if (in_array($rig, array(")", ","))
498 && (is_array($mid) && ($mid[0] == T_CONSTANT_ENCAPSED_STRING))) {
499 // By default, there is no context.
500 $context = POTX_CONTEXT_NONE;
501 if ($context_support && ($rig == ',')) {
502 // If there was a comma after the string, we need to look forward
503 // to try and find the context.
504 $context = potx_parser_util_context($ti, $ti + 4, $display_name, $function_name);
505 }
506 if ($context !== POTX_CONTEXT_ERROR) {
507 // Only save if there was no error in context parsing.
508 $string_save_callback(potx_parser_util_format_string($mid[1]), $context, $display_name, $line, $string_mode);
509 }
510 else {
511 // $function_name() found, but the context is not good.
512 potx_parser_util_token_error($display_name, $line, $function_name, $ti, t('The context specified for @function() is not valid.', array('@function' => $function_name)));
513 }
514 }
515 else {
516 // $function_name() found, but inside is something which is not a string literal.
517 potx_parser_util_token_error($display_name, $line, $function_name, $ti, t('The first parameter to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
518 }
519 }
520 }
521 }
522 }
523
524 /**
525 * Detect all occurances of watchdog() calls. Only from Drupal 6.
526 *
527 * These sequences are searched for:
528 * watchdog + "(" + T_CONSTANT_ENCAPSED_STRING + "," +
529 * T_CONSTANT_ENCAPSED_STRING + something
530 *
531 * @param $display_name
532 * Name of file parsed.
533 * @param $string_save_callback
534 * Callback function used to save strings.
535 */
536 function potx_parser_find_watchdog_strings($display_name, $string_save_callback) {
537 global $_potx_tokens, $_potx_lookup;
538
539 // Lookup tokens by function name.
540 if (isset($_potx_lookup['watchdog'])) {
541 foreach ($_potx_lookup['watchdog'] as $ti) {
542 list($ctok, $par, $mtype, $comma, $message, $rig) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1], $_potx_tokens[$ti+2], $_potx_tokens[$ti+3], $_potx_tokens[$ti+4], $_potx_tokens[$ti+5]);
543 list($type, $string, $line) = $ctok;
544 if ($par == '(') {
545 // Both type and message should be a string literal.
546 if (in_array($rig, array(')', ',')) && $comma == ','
547 && (is_array($mtype) && ($mtype[0] == T_CONSTANT_ENCAPSED_STRING))
548 && (is_array($message) && ($message[0] == T_CONSTANT_ENCAPSED_STRING))) {
549 // Context is not supported on watchdog().
550 $string_save_callback(potx_parser_util_format_string($mtype[1]), POTX_CONTEXT_NONE, $display_name, $line);
551 $string_save_callback(potx_parser_util_format_string($message[1]), POTX_CONTEXT_NONE, $display_name, $line);
552 }
553 else {
554 // watchdog() found, but inside is something which is not a string literal.
555 potx_parser_util_token_error($display_name, $line, 'watchdog', $ti, t('The first two watchdog() parameters should be literal strings. There should be no variables, concatenation, constants or even a t() call there.'), 'http://drupal.org/node/323101');
556 }
557 }
558 }
559 }
560 }
561
562 /**
563 * Detect all occurances of format_plural calls.
564 *
565 * These sequences are searched for:
566 * T_STRING("format_plural") + "(" + ..anything (might be more tokens).. +
567 * "," + T_CONSTANT_ENCAPSED_STRING +
568 * "," + T_CONSTANT_ENCAPSED_STRING + parenthesis (or comma allowed from
569 * Drupal 6)
570 *
571 * @param $display_name
572 * Name of file parsed.
573 * @param $string_save_callback
574 * Callback function used to save strings.
575 * @param $api_version
576 * Drupal API version to work with.
577 */
578 function potx_parser_find_format_plural_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
579 global $_potx_tokens, $_potx_lookup;
580
581 if (isset($_potx_lookup['format_plural'])) {
582 foreach ($_potx_lookup['format_plural'] as $ti) {
583 list($ctok, $par1) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1]);
584 list($type, $string, $line) = $ctok;
585 if ($par1 == "(") {
586 // Eat up everything that is used as the first parameter
587 $tn = $ti + 2;
588 $depth = 0;
589 while (!($_potx_tokens[$tn] == "," && $depth == 0)) {
590 if ($_potx_tokens[$tn] == "(") {
591 $depth++;
592 }
593 elseif ($_potx_tokens[$tn] == ")") {
594 $depth--;
595 }
596 $tn++;
597 }
598 // Get further parameters
599 list($comma1, $singular, $comma2, $plural, $par2) = array($_potx_tokens[$tn], $_potx_tokens[$tn+1], $_potx_tokens[$tn+2], $_potx_tokens[$tn+3], $_potx_tokens[$tn+4]);
600 if (($comma2 == ',') && ($par2 == ')' || ($par2 == ',' && $api_version > POTX_API_5)) &&
601 (is_array($singular) && ($singular[0] == T_CONSTANT_ENCAPSED_STRING)) &&
602 (is_array($plural) && ($plural[0] == T_CONSTANT_ENCAPSED_STRING))) {
603 // By default, there is no context.
604 $context = POTX_CONTEXT_NONE;
605 if ($par2 == ',' && ($api_version > POTX_API_6)) {
606 // If there was a comma after the plural, we need to look forward
607 // to try and find the context.
608 $context = potx_parser_util_context($ti, $tn + 5, $display_name, 'format_plural');
609 }
610 if ($context !== POTX_CONTEXT_ERROR) {
611 // Only save if there was no error in context parsing.
612 $string_save_callback(
613 potx_parser_util_format_string($singular[1]) . "\0" . potx_parser_util_format_string($plural[1]),
614 $context,
615 $display_name,
616 $line
617 );
618 }
619 else {
620 // format_plural() found, but the context is not good.
621 potx_parser_util_token_error($display_name, $line, 'format_plural', $ti, t('The context specified for format_plural() is not valid.'));
622 }
623 }
624 else {
625 // format_plural() found, but the parameters are not correct.
626 potx_parser_util_token_error($display_name, $line, 'format_plural', $ti, t('In format_plural(), the singular and plural strings should be literal strings. There should be no variables, concatenation, constants or even a t() call there.'), 'http://drupal.org/node/323072');
627 }
628 }
629 }
630 }
631 }
632
633 /**
634 * Detect permission names from the hook_perm() implementations.
635 *
636 * Note that this will get confused with a similar pattern in a comment,
637 * and with dynamic permissions, which need to be accounted for.
638 *
639 * @param $display_name
640 * Full path name of file parsed.
641 * @param $filebase
642 * Filenaname of file parsed.
643 * @param $string_save_callback
644 * Callback function used to save strings.
645 */
646 function potx_parser_find_perm_strings($display_name, $filebase, $string_save_callback) {
647 global $_potx_tokens, $_potx_lookup;
648
649 if (isset($_potx_lookup[$filebase . '_perm'])) {
650 // Special case for node module, because it uses dynamic permissions.
651 // Include the static permissions by hand. That's about all we can do here.
652 if ($filebase == 'node') {
653 $line = $_potx_tokens[$_potx_lookup['node_perm'][0]][2];
654 // List from node.module 1.763 (checked in on 2006/12/29 at 21:25:36 by drumm)
655 $nodeperms = array('administer content types', 'administer nodes', 'access content', 'view revisions', 'revert revisions');
656 foreach ($nodeperms as $item) {
657 // hook_perm() is only ever found on a Drupal system which does not
658 // support context.
659 $string_save_callback($item, POTX_CONTEXT_NONE, $display_name, $line);
660 }
661 }
662 else {
663 $count = 0;
664 foreach ($_potx_lookup[$filebase . '_perm'] as $ti) {
665 $tn = $ti;
666 while (is_array($_potx_tokens[$tn]) || $_potx_tokens[$tn] != '}') {
667 if (is_array($_potx_tokens[$tn]) && $_potx_tokens[$tn][0] == T_CONSTANT_ENCAPSED_STRING) {
668 // hook_perm() is only ever found on a Drupal system which does not
669 // support context.
670 $string_save_callback(potx_parser_util_format_string($_potx_tokens[$tn][1]), POTX_CONTEXT_NONE, $display_name, $_potx_tokens[$tn][2]);
671 $count++;
672 }
673 $tn++;
674 }
675 }
676 if (!$count) {
677 potx_parser_util_set_message(t('%hook should have an array of literal string permission names.', array('%hook' => $filebase . '_perm()')), $display_name, NULL, NULL, 'http://drupal.org/node/323101');
678 }
679 }
680 }
681 }
682
683 /**
684 * List of menu item titles. Only from Drupal 6.
685 *
686 * @param $display_name
687 * Full path name of file parsed.
688 * @param $filebase
689 * Filenaname of file parsed.
690 * @param $string_save_callback
691 * Callback function used to save strings.
692 */
693 function potx_parser_find_menu_strings($display_name, $filebase, $string_save_callback) {
694 global $_potx_tokens, $_potx_lookup;
695
696 $hooks = array('_menu', '_menu_alter');
697 $keys = array("'title'", '"title"', "'description'", '"description"');
698
699 foreach ($hooks as $hook) {
700 if (isset($_potx_lookup[$filebase . $hook]) && is_array($_potx_lookup[$filebase . $hook])) {
701 // We have this menu hook in this file.
702 foreach ($_potx_lookup[$filebase . $hook] as $ti) {
703 $end = potx_parser_util_end_of_function($ti);
704 $tn = $ti;
705 while ($tn < $end) {
706
707 // Support for array syntax more commonly used in menu hooks:
708 // $items = array('node/add' => array('title' => 'Add content'));
709 if ($_potx_tokens[$tn][0] == T_CONSTANT_ENCAPSED_STRING && in_array($_potx_tokens[$tn][1], $keys) && $_potx_tokens[$tn+1][0] == T_DOUBLE_ARROW) {
710 if ($_potx_tokens[$tn+2][0] == T_CONSTANT_ENCAPSED_STRING) {
711 // We cannot export menu item context.
712 $string_save_callback(
713 potx_parser_util_format_string($_potx_tokens[$tn+2][1]),
714 POTX_CONTEXT_NONE,
715 $display_name,
716 $_potx_tokens[$tn+2][2]
717 );
718 $tn+=2; // Jump forward by 2.
719 }
720 else {
721 potx_parser_util_set_message(t('Invalid menu %element definition found in %hook. Title and description keys of the menu array should be literal strings.', array('%element' => $_potx_tokens[$tn][1], '%hook' => $filebase . $hook . '()')), $display_name, $_potx_tokens[$tn][2], NULL, 'http://drupal.org/node/323101');
722 }
723 }
724
725 // Support for array syntax more commonly used in menu alters:
726 // $items['node/add']['title'] = 'Add content here';
727 if (is_string($_potx_tokens[$tn]) && $_potx_tokens[$tn] == '[' && $_potx_tokens[$tn+1][0] == T_CONSTANT_ENCAPSED_STRING && in_array($_potx_tokens[$tn+1][1], $keys) && is_string($_potx_tokens[$tn+2]) && $_potx_tokens[$tn+2] == ']') {
728 if (is_string($_potx_tokens[$tn+3]) && $_potx_tokens[$tn+3] == '=' && $_potx_tokens[$tn+4][0] == T_CONSTANT_ENCAPSED_STRING) {
729 // We cannot export menu item context.
730 $string_save_callback(
731 potx_parser_util_format_string($_potx_tokens[$tn+4][1]),
732 POTX_CONTEXT_NONE,
733 $display_name,
734 $_potx_tokens[$tn+4][2]
735 );
736 $tn+=4; // Jump forward by 4.
737 }
738 else {
739 potx_parser_util_set_message(t('Invalid menu %element definition found in %hook. Title and description keys of the menu array should be literal strings.', array('%element' => $_potx_tokens[$tn+1][1], '%hook' => $filebase . $hook . '()')), $display_name, $_potx_tokens[$tn+1][2], NULL, 'http://drupal.org/node/323101');
740 }
741 }
742 $tn++;
743 }
744 }
745 }
746 }
747 }
748
749 /**
750 * Get languages names from Drupal's locale.inc.
751 *
752 * @param $display_name
753 * Full path name of file parsed
754 * @param $string_save_callback
755 * Callback function used to save strings.
756 * @param $api_version
757 * Drupal API version to work with.
758 */
759 function potx_parser_find_language_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
760 global $_potx_tokens, $_potx_lookup;
761
762 foreach ($_potx_lookup[$api_version > POTX_API_5 ? '_locale_get_predefined_list' : '_locale_get_iso639_list'] as $ti) {
763 // Search for the definition of _locale_get_predefined_list(), not where it is called.
764 if ($_potx_tokens[$ti-1][0] == T_FUNCTION) {
765 break;
766 }
767 }
768
769 $end = potx_parser_util_end_of_function($ti);
770 $ti += 7; // function name, (, ), {, return, array, (
771 while ($ti < $end) {
772 while ($_potx_tokens[$ti][0] != T_ARRAY) {
773 if (!is_array($_potx_tokens[$ti]) && $_potx_tokens[$ti] == ';') {
774 // We passed the end of the list, break out to function level
775 // to prevent an infinite loop.
776 break 2;
777 }
778 $ti++;
779 }
780 $ti += 2; // array, (
781 // Language names are context-less.
782 $string_save_callback(potx_parser_util_format_string($_potx_tokens[$ti][1]), POTX_CONTEXT_NONE, $display_name, $_potx_tokens[$ti][2]);
783 }
784 }
785
786 // === Standard set of string additions ========================================
787
788 /**
789 * Add date strings, which cannot be extracted otherwise.
790 *
791 * This is called for locale.module.
792 *
793 * @param $display_name
794 * Name of the file parsed.
795 * @param $string_save_callback
796 * Callback function used to save strings.
797 * @param $api_version
798 * Drupal API version to work with.
799 */
800 function potx_parser_add_date_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
801 for ($i = 1; $i <= 12; $i++) {
802 $stamp = mktime(0, 0, 0, $i, 1, 1971);
803 if ($api_version > POTX_API_6) {
804 // From Drupal 7, long month names are saved with this context.
805 $string_save_callback(date("F", $stamp), 'Long month name', $display_name);
806 }
807 elseif ($api_version > POTX_API_5) {
808 // Drupal 6 uses a little hack. No context.
809 $string_save_callback('!long-month-name ' . date("F", $stamp), POTX_CONTEXT_NONE, $display_name);
810 }
811 else {
812 // Older versions just accept the confusion, no context.
813 $string_save_callback(date("F", $stamp), POTX_CONTEXT_NONE, $display_name);
814 }
815 // Short month names lack a context anyway.
816 $string_save_callback(date("M", $stamp), POTX_CONTEXT_NONE, $display_name);
817 }
818 for ($i = 0; $i <= 7; $i++) {
819 $stamp = $i * 86400;
820 $string_save_callback(date("D", $stamp), POTX_CONTEXT_NONE, $display_name);
821 $string_save_callback(date("l", $stamp), POTX_CONTEXT_NONE, $display_name);
822 }
823 $string_save_callback('am', POTX_CONTEXT_NONE, $display_name);
824 $string_save_callback('pm', POTX_CONTEXT_NONE, $display_name);
825 $string_save_callback('AM', POTX_CONTEXT_NONE, $display_name);
826 $string_save_callback('PM', POTX_CONTEXT_NONE, $display_name);
827 }
828
829 /**
830 * Add format_interval special strings.
831 *
832 * These cannot be extracted otherwise. This is called for common.inc
833 *
834 * @param $display_name
835 * Name of the file parsed.
836 * @param $string_save_callback
837 * Callback function used to save strings.
838 * @param $api_version
839 * Drupal API version to work with.
840 */
841 function potx_parser_add_format_interval_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
842 $components = array(
843 '1 year' => '@count years',
844 '1 week' => '@count weeks',
845 '1 day' => '@count days',
846 '1 hour' => '@count hours',
847 '1 min' => '@count min',
848 '1 sec' => '@count sec'
849 );
850 if ($api_version > POTX_API_6) {
851 // Month support added in Drupal 7.
852 $components['1 month'] = '@count months';
853 }
854
855 foreach ($components as $singular => $plural) {
856 // Intervals support no context.
857 $string_save_callback($singular . "\0" . $plural, POTX_CONTEXT_NONE, $display_name);
858 }
859 }
860
861 /**
862 * Add default theme region names, which cannot be extracted otherwise.
863 *
864 * These default names are defined in system.module
865 *
866 * @param $display_name
867 * Name of the file parsed.
868 * @param $string_save_callback
869 * Callback function used to save strings.
870 * @param $api_version
871 * Drupal API version to work with.
872 */
873 function potx_parser_add_default_region_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
874 $regions = array(
875 'Left sidebar',
876 'Right sidebar',
877 'Content',
878 'Header',
879 'Footer',
880 );
881 if ($api_version > POTX_API_6) {
882 $regions[] = 'Highlighted';
883 $regions[] = 'Help';
884 $regions[] = 'Page top';
885 $regions[] = 'Page bottom';
886 }
887 foreach ($regions as $region) {
888 // Regions come with the default context.
889 $string_save_callback($region, POTX_CONTEXT_NONE, $display_name);
890 }
891 }
892
893 // === Non-token based parsing functions =======================================
894
895 /**
896 * Parse an .info file and add relevant strings to the list.
897 *
898 * @param $file_path
899 * Complete file path to load contents with.
900 * @param $display_name
901 * Display oriented file name for saved string data and error messages.
902 * @param $string_save_callback
903 * Callback function to use to save the collected strings.
904 * @param $api_version
905 * Drupal API version to work with.
906 */
907 function potx_parser_get_info_strings($file_path, $display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
908 $info = array();
909
910 if (file_exists($file_path)) {
911 // Drupal 6+ has drupal_parse_info_file() and a special .info format.
912 $info = $api_version > POTX_API_5 ? drupal_parse_info_file($file_path) : parse_ini_file($file_path);
913 }
914
915 // We need the name, description and package values. Others,
916 // like core and PHP compatibility, timestamps or versions
917 // are not to be translated.
918 foreach (array('name', 'description', 'package') as $key) {
919 if (isset($info[$key])) {
920 // No context support for .info file strings.
921 $string_save_callback(addcslashes($info[$key], "\0..\37\\\""), POTX_CONTEXT_NONE, $display_name);
922 }
923 }
924
925 // Add regions names from themes.
926 if (isset($info['regions']) && is_array($info['regions'])) {
927 foreach ($info['regions'] as $region => $region_name) {
928 // No context support for .info file strings.
929 $string_save_callback(addcslashes($region_name, "\0..\37\\\""), POTX_CONTEXT_NONE, $display_name);
930 }
931 }
932 }
933
934 /**
935 * Parse a JavaScript file for translatables. Only from Drupal 6.
936 *
937 * Extracts strings wrapped in Drupal.t() and Drupal.formatPlural()
938 * calls and inserts them into potx storage.
939 *
940 * Regex code lifted from _locale_parse_js_file().
941 */
942 function potx_parser_get_js_strings($code, $display_name, $string_save_callback) {
943 $js_string_regex = '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+';
944
945 // Match all calls to Drupal.t() in an array.
946 // Note: \s also matches newlines with the 's' modifier.
947 preg_match_all('~[^\w]Drupal\s*\.\s*t\s*\(\s*(' . $js_string_regex . ')\s*[,\)]~s', $code, $t_matches, PREG_SET_ORDER);
948 if (isset($t_matches) && count($t_matches)) {
949 foreach ($t_matches as $match) {
950 // Remove match from code to help us identify faulty Drupal.t() calls.
951 $code = str_replace($match[0], '', $code);
952 // @todo: figure out how to parse out context, once Drupal supports it.
953 $string_save_callback(potx_parser_get_js_string($match[1]), POTX_CONTEXT_NONE, $display_name, 0);
954 }
955 }
956
957 // Match all Drupal.formatPlural() calls in another array.
958 preg_match_all('~[^\w]Drupal\s*\.\s*formatPlural\s*\(\s*.+?\s*,\s*(' . $js_string_regex . ')\s*,\s*((?:(?:\'(?:\\\\\'|[^\'])*@count(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*@count(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s*[,\)]~s', $code, $plural_matches, PREG_SET_ORDER);
959 if (isset($plural_matches) && count($plural_matches)) {
960 foreach ($plural_matches as $index => $match) {
961 // Remove match from code to help us identify faulty
962 // Drupal.formatPlural() calls later.
963 $code = str_replace($match[0], '', $code);
964 // @todo: figure out how to parse out context, once Drupal supports it.
965 $string_save_callback(
966 potx_parser_get_js_string($match[1]) . "\0" . potx_parser_get_js_string($match[2]),
967 POTX_CONTEXT_NONE,
968 $display_name,
969 0
970 );
971 }
972 }
973
974 // Any remaining Drupal.t() or Drupal.formatPlural() calls are evil. This
975 // regex is not terribly accurate (ie. code wrapped inside will confuse
976 // the match), but we only need some unique part to identify the faulty calls.
977 preg_match_all('~[^\w]Drupal\s*\.\s*(t|formatPlural)\s*\([^)]+\)~s', $code, $faulty_matches, PREG_SET_ORDER);
978 if (isset($faulty_matches) && count($faulty_matches)) {
979 foreach ($faulty_matches as $index => $match) {
980 $message = ($match[1] == 't') ? t('Drupal.t() calls should have a single literal string as their first parameter.') : t('The singular and plural string parameters on Drupal.formatPlural() calls should be literal strings, plural containing a @count placeholder.');
981 potx_parser_util_set_message($message, $display_name, NULL, $match[0], 'http://drupal.org/node/323109');
982 }
983 }
984 }
985
986 /**
987 * Clean up string found in JavaScript source code. Only from Drupal 6.
988 */
989 function potx_parser_get_js_string($string) {
990 return potx_parser_util_format_string(implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', $string)));
991 }
992
993 /**
994 * Get the exact CVS version number from a file.
995 *
996 * @todo This will be obsoleted by the drupal.org GIT migration.
997 *
998 * @param $code
999 * Complete source code of the file parsed.
1000 * @param $display_name
1001 * Name of the file parsed (to be passed to $file_version_callback).
1002 * @param $file_version_callback
1003 * Callback used to save the version information.
1004 */
1005 function potx_parser_get_version_number($code, $display_name, $file_version_callback) {
1006 // Prevent CVS from replacing this pattern with actual info.
1007 if (preg_match('!\\$I' . 'd: ([^\\$]+) Exp \\$!', $code, $version_info)) {
1008 $file_version_callback($version_info[1], $display_name);
1009 }
1010 else {
1011 // Unknown version information.
1012 $file_version_callback($display_name . ': n/a', $display_name);
1013 }
1014 }
1015
1016 /**
1017 * Default $file_version_callback used by the potx system.
1018 *
1019 * Saves values to a global array to reduce memory consumption problems when
1020 * passing around big chunks of values.
1021 *
1022 * @param $value
1023 * The version number value of $file. If NULL, the collected
1024 * values are returned.
1025 * @param $display_name
1026 * Name of file where the version information was found.
1027 */
1028 function potx_parser_callback_version($value = NULL, $display_name = NULL) {
1029 global $_potx_versions;
1030
1031 if (isset($value)) {
1032 $_potx_versions[$display_name] = $value;
1033 }
1034 else {
1035 return $_potx_versions;
1036 }
1037 }
1038
1039 // === Default file parse callbacks ============================================
1040
1041 /**
1042 * Default $string_save_callback used by the potx system.
1043 *
1044 * Saves values to global arrays to reduce memory consumption problems when
1045 * passing around big chunks of values.
1046 *
1047 * @param $value
1048 * The string value. If NULL, the array of collected values
1049 * are returned for the given $string_mode.
1050 * @param $context
1051 * From Drupal 7, separate contexts are supported. POTX_CONTEXT_NONE is
1052 * the default, if the code does not specify a context otherwise.
1053 * @param $display_name
1054 * Name of file where the string was found.
1055 * @param $line
1056 * Line number where the string was found.
1057 * @param $string_mode
1058 * String mode: POTX_STRING_INSTALLER, POTX_STRING_RUNTIME
1059 * or POTX_STRING_BOTH.
1060 */
1061 function potx_parser_callback_save_string($value = NULL, $context = NULL, $display_name = NULL, $line = 0, $string_mode = POTX_STRING_RUNTIME) {
1062 global $_potx_strings, $_potx_install;
1063
1064 if (isset($value)) {
1065
1066 // Value set but empty. Mark error on empty translatable string. Only trim
1067 // for empty string checking, since we should store leading/trailing
1068 // whitespace as it appears in the string otherwise.
1069 $check_empty = trim($value);
1070 if (empty($check_empty)) {
1071 potx_parser_util_set_message(t('Empty string attempted to be localized. Please do not leave test code for localization in your source.'), $display_name, $line);
1072 return;
1073 }
1074
1075 switch ($string_mode) {
1076 case POTX_STRING_BOTH:
1077 // Mark installer strings as duplicates of runtime strings if
1078 // the string was both recorded in the runtime and in the installer.
1079 $_potx_install[$value][$context][$display_name][] = $line . ' (dup)';
1080 // Break intentionally missing.
1081 case POTX_STRING_RUNTIME:
1082 // Mark runtime strings as duplicates of installer strings if
1083 // the string was both recorded in the runtime and in the installer.
1084 $_potx_strings[$value][$context][$display_name][] = $line . ($string_mode == POTX_STRING_BOTH ? ' (dup)' : '');
1085 break;
1086 case POTX_STRING_INSTALLER:
1087 $_potx_install[$value][$context][$display_name][] = $line;
1088 break;
1089 }
1090 }
1091 else {
1092 return ($string_mode == POTX_STRING_RUNTIME ? $_potx_strings : $_potx_install);
1093 }
1094 }
1095 /**
1096 * Returns a header generated for a given file.
1097 *
1098 * @param $template_export_langcode
1099 * Language code if the template should have language dependent content
1100 * (like plural formulas and language name) included.
1101 * @param $api_version
1102 * Drupal API version to work with.
1103 */
1104 function potx_parser_callback_header($template_export_langcode = NULL, $api_version = POTX_API_CURRENT) {
1105 // We only have language to use if we should export with that langcode.
1106 $language = NULL;
1107 if (isset($template_export_langcode)) {
1108 $language = db_query($api_version > POTX_API_5 ? "SELECT language, name, plurals, formula FROM {languages} WHERE language = :langcode" : "SELECT locale, name, plurals, formula FROM {locales_meta} WHERE locale = :langcode", array(':langcode' => $template_export_langcode))->fetchObject();
1109 }
1110
1111 $output = '# $' . 'Id' . '$' . "\n";
1112 $output .= "#\n";
1113 $output .= '# ' . (isset($language) ? $language->name : 'LANGUAGE') . " translation\n";
1114 $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
1115 $output .= "# --VERSIONS--\n";
1116 $output .= "#\n";
1117 $output .= "#, fuzzy\n";
1118 $output .= "msgid \"\"\n";
1119 $output .= "msgstr \"\"\n";
1120 $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
1121 $output .= '"POT-Creation-Date: ' . date("Y-m-d H:iO") . "\\n\"\n";
1122 $output .= '"PO-Revision-Date: ' . (isset($language) ? date("Y-m-d H:iO") : 'YYYY-mm-DD HH:MM+ZZZZ') . "\\n\"\n";
1123 $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
1124 $output .= "\"Language-Team: " . (isset($language) ? $language->name : 'LANGUAGE') . " <EMAIL@ADDRESS>\\n\"\n";
1125 $output .= "\"MIME-Version: 1.0\\n\"\n";
1126 $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
1127 $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
1128 if (isset($language->formula) && isset($language->plurals)) {
1129 $output .= "\"Plural-Forms: nplurals=" . $language->plurals . "; plural=" . strtr($language->formula, array('$' => '')) . ";\\n\"\n\n";
1130 }
1131 else {
1132 $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
1133 }
1134 return $output;
1135 }
1136
1137 // === Utility functions =======================================================
1138
1139 /**
1140 * Helper function to look up the token closing the current function.
1141 *
1142 * @param $here
1143 * The token at the function name
1144 */
1145 function potx_parser_util_end_of_function($here) {
1146 global $_potx_tokens;
1147
1148 // Seek to open brace.
1149 while (is_array($_potx_tokens[$here]) || $_potx_tokens[$here] != '{') {
1150 $here++;
1151 }
1152 $nesting = 1;
1153 while ($nesting > 0) {
1154 $here++;
1155 if (!is_array($_potx_tokens[$here])) {
1156 if ($_potx_tokens[$here] == '}') {
1157 $nesting--;
1158 }
1159 if ($_potx_tokens[$here] == '{') {
1160 $nesting++;
1161 }
1162 }
1163 }
1164 return $here;
1165 }
1166
1167 /**
1168 * Helper to move past t() and format_plural() arguments in search of context.
1169 *
1170 * @param $here
1171 * The token before the start of the arguments
1172 */
1173 function potx_parser_util_skip_args($here) {
1174 global $_potx_tokens;
1175
1176 $nesting = 0;
1177 // Go through to either the end of the function call or to a comma
1178 // after the current position on the same nesting level.
1179 while (!(($_potx_tokens[$here] == ',' && $nesting == 0) ||
1180 ($_potx_tokens[$here] == ')' && $nesting == -1))) {
1181 $here++;
1182 if (!is_array($_potx_tokens[$here])) {
1183 if ($_potx_tokens[$here] == ')') {
1184 $nesting--;
1185 }
1186 if ($_potx_tokens[$here] == '(') {
1187 $nesting++;
1188 }
1189 }
1190 }
1191 // If we run out of nesting, it means we reached the end of the function call,
1192 // so we skipped the arguments but did not find meat for looking at the
1193 // specified context.
1194 return ($nesting == 0 ? $here : FALSE);
1195 }
1196
1197 /**
1198 * Helper to find the value for 'context' on t() and format_plural().
1199 *
1200 * @param $tf
1201 * Start position of the original function.
1202 * @param $ti
1203 * Start position where we should search from.
1204 * @param $file
1205 * Full path name of file parsed.
1206 * @param function_name
1207 * The name of the function to look for. Either 'format_plural' or 't'
1208 * given that Drupal 7 only supports context on these.
1209 */
1210 function potx_parser_util_context($tf, $ti, $file, $function_name) {
1211 global $_potx_tokens;
1212
1213 // Start from after the comma and skip the possible arguments for the function
1214 // so we can look for the context.
1215 if (($ti = potx_parser_util_skip_args($ti)) && ($_potx_tokens[$ti] == ',')) {
1216 // Now we actually might have some definition for a context. The $options
1217 // argument is coming up, which might have a key for context.
1218 list($com, $arr, $par) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1], $_potx_tokens[$ti+2]);
1219 if ($com == ',' && $arr[1] == 'array' && $par == '(') {
1220 $nesting = 0;
1221 $ti += 3;
1222 // Go through to either the end of the array or to the key definition of
1223 // context on the same nesting level.
1224 while (!((is_array($_potx_tokens[$ti]) && (in_array($_potx_tokens[$ti][1], array('"context"', "'context'"))) && ($_potx_tokens[$ti][0] == T_CONSTANT_ENCAPSED_STRING) && ($nesting == 0)) ||
1225 ($_potx_tokens[$ti] == ')' && $nesting == -1))) {
1226 $ti++;
1227 if (!is_array($_potx_tokens[$ti])) {
1228 if ($_potx_tokens[$ti] == ')') {
1229 $nesting--;
1230 }
1231 if ($_potx_tokens[$ti] == '(') {
1232 $nesting++;
1233 }
1234 }
1235 }
1236 if ($nesting == 0) {
1237 // Found the 'context' key on the top level of the $options array.
1238 list($arw, $str) = array($_potx_tokens[$ti+1], $_potx_tokens[$ti+2]);
1239 if (is_array($arw) && $arw[1] == '=>' && is_array($str) && $str[0] == T_CONSTANT_ENCAPSED_STRING) {
1240 return potx_parser_util_format_string($str[1]);
1241 }
1242 else {
1243 list($type, $string, $line) = $_potx_tokens[$ti];
1244 // @todo: fix error reference.
1245 potx_parser_util_token_error($file, $line, $function_name, $tf, t('The context element in the options array argument to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
1246 // Return with error.
1247 return POTX_CONTEXT_ERROR;
1248 }
1249 }
1250 else {
1251 // Did not found 'context' key in $options array.
1252 return POTX_CONTEXT_NONE;
1253 }
1254 }
1255 }
1256
1257 // After skipping args, we did not find a comma to look for $options.
1258 return POTX_CONTEXT_NONE;
1259 }
1260
1261 /**
1262 * Escape quotes in a strings depending on the surrounding quote type used.
1263 *
1264 * @param $str
1265 * The strings to escape
1266 */
1267 function potx_parser_util_format_string($str) {
1268 $quo = substr($str, 0, 1);
1269 $str = substr($str, 1, -1);
1270 if ($quo == '"') {
1271 $str = stripcslashes($str);
1272 }
1273 else {
1274 $str = strtr($str, array("\\'" => "'", "\\\\" => "\\"));
1275 }
1276 return addcslashes($str, "\0..\37\\\"");
1277 }
1278
1279 /**
1280 * Save an error with an extract of where the error was found.
1281 *
1282 * @param $display_name
1283 * Display name of file.
1284 * @param $line
1285 * Line number of error.
1286 * @param $marker
1287 * Function name with which the error was identified.
1288 * @param $ti
1289 * Index on the token array.
1290 * @param $error
1291 * Helpful error message for users.
1292 * @param $docs_url
1293 * Optional documentation reference.
1294 */
1295 function potx_parser_util_token_error($file, $line, $marker, $ti, $error, $docs_url = NULL) {
1296 global $_potx_tokens;
1297
1298 $tokens = '';
1299 $ti += 2;
1300 $tc = count($_potx_tokens);
1301 $par = 1;
1302 while ((($tc - $ti) > 0) && $par) {
1303 if (is_array($_potx_tokens[$ti])) {
1304 $tokens .= $_potx_tokens[$ti][1];
1305 }
1306 else {
1307 $tokens .= $_potx_tokens[$ti];
1308 if ($_potx_tokens[$ti] == "(") {
1309 $par++;
1310 }
1311 elseif ($_potx_tokens[$ti] == ")") {
1312 $par--;
1313 }
1314 }
1315 $ti++;
1316 }
1317 potx_parser_util_set_message($error, $file, $line, $marker . '(' . $tokens, $docs_url);
1318 }
1319
1320 /**
1321 * Parse error message collection function.
1322 *
1323 * @param $value
1324 * Error message or NULL to get the errors collected (and reset the list).
1325 * @param $file
1326 * Name of file the error message is related to.
1327 * @param $line
1328 * Number of line the error message is related to.
1329 * @param $excerpt
1330 * Excerpt of the code in question, if available.
1331 * @param $docs_url
1332 * URL to the guidelines to follow to fix the problem.
1333 */
1334 function potx_parser_util_set_message($value = NULL, $file = NULL, $line = NULL, $excerpt = NULL, $docs_url = NULL) {
1335 static $messages = array();
1336
1337 if (empty($value)) {
1338 $errors = $messages;
1339 $messages = array();
1340 return $errors;
1341 }
1342
1343 $location_info = '';
1344 if (isset($file)) {
1345 if (isset($line)) {
1346 if (isset($excerpt)) {
1347 $location_info = t('At %excerpt in %file on line %line.', array('%excerpt' => $excerpt, '%file' => $file, '%line' => $line));
1348 }
1349 else {
1350 $location_info = t('In %file on line %line.', array('%file' => $file, '%line' => $line));
1351 }
1352 }
1353 else {
1354 if (isset($excerpt)) {
1355 $location_info = t('At %excerpt in %file.', array('%excerpt' => $excerpt, '%file' => $file));
1356 }
1357 else {
1358 $location_info = t('In %file.', array('%file' => $file));
1359 }
1360 }
1361 }
1362
1363 // Documentation helpers are provided as readable text in most modes.
1364 $read_more = '';
1365 if (isset($docs_url)) {
1366 $read_more = t('Read more at <a href="@url">@url</a>', array('@url' => $docs_url));
1367 }
1368
1369 $readable_error_message = join(' ', array($value, $location_info, $read_more));
1370 $messages[] = array($value, $file, $line, $excerpt, $docs_url, $readable_error_message);
1371 }
1372
1373 /**
1374 * Get the list of messages saved when parsing one or more files.
1375 *
1376 * Also cleans off the list of messages.
1377 */
1378 function potx_parser_util_get_messages() {
1379 return potx_parser_util_set_message();
1380 }
1381
1382 /**
1383 * Reset global variables used for storing cross-function data.
1384 */
1385 function potx_parser_util_reset() {
1386 global $_potx_strings, $_potx_install, $_potx_versions;
1387 $_potx_strings = $_potx_install = $_potx_versions = array();
1388 }