| [ Index ] |
PHP Cross Reference of Drupal 6 (gatewave) |
[Summary view] [Print] [Text view]
1 <?php 2 // $Id: locale.inc,v 1.174.2.14 2010/08/11 19:42:26 goba Exp $ 3 4 /** 5 * @file 6 * Administration functions for locale.module. 7 */ 8 9 define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+'); 10 11 /** 12 * Translation import mode overwriting all existing translations 13 * if new translated version available. 14 */ 15 define('LOCALE_IMPORT_OVERWRITE', 0); 16 17 /** 18 * Translation import mode keeping existing translations and only 19 * inserting new strings. 20 */ 21 define('LOCALE_IMPORT_KEEP', 1); 22 23 /** 24 * @defgroup locale-language-overview Language overview functionality 25 * @{ 26 */ 27 28 /** 29 * User interface for the language overview screen. 30 */ 31 function locale_languages_overview_form() { 32 $languages = language_list('language', TRUE); 33 34 $options = array(); 35 $form['weight'] = array('#tree' => TRUE); 36 foreach ($languages as $langcode => $language) { 37 // Language code should contain no markup, but is emitted 38 // by radio and checkbox options. 39 $langcode = check_plain($langcode); 40 41 $options[$langcode] = ''; 42 if ($language->enabled) { 43 $enabled[] = $langcode; 44 } 45 $form['weight'][$langcode] = array( 46 '#type' => 'weight', 47 '#default_value' => $language->weight 48 ); 49 $form['name'][$langcode] = array('#value' => check_plain($language->name)); 50 $form['native'][$langcode] = array('#value' => check_plain($language->native)); 51 $form['direction'][$langcode] = array('#value' => ($language->direction == LANGUAGE_RTL ? t('Right to left') : t('Left to right'))); 52 } 53 $form['enabled'] = array('#type' => 'checkboxes', 54 '#options' => $options, 55 '#default_value' => $enabled, 56 ); 57 $form['site_default'] = array('#type' => 'radios', 58 '#options' => $options, 59 '#default_value' => language_default('language'), 60 ); 61 $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); 62 $form['#theme'] = 'locale_languages_overview_form'; 63 64 return $form; 65 } 66 67 /** 68 * Theme the language overview form. 69 * 70 * @ingroup themeable 71 */ 72 function theme_locale_languages_overview_form($form) { 73 $default = language_default(); 74 foreach ($form['name'] as $key => $element) { 75 // Do not take form control structures. 76 if (is_array($element) && element_child($key)) { 77 // Disable checkbox for the default language, because it cannot be disabled. 78 if ($key == $default->language) { 79 $form['enabled'][$key]['#attributes']['disabled'] = 'disabled'; 80 } 81 $rows[] = array( 82 array('data' => drupal_render($form['enabled'][$key]), 'align' => 'center'), 83 check_plain($key), 84 '<strong>'. drupal_render($form['name'][$key]) .'</strong>', 85 drupal_render($form['native'][$key]), 86 drupal_render($form['direction'][$key]), 87 drupal_render($form['site_default'][$key]), 88 drupal_render($form['weight'][$key]), 89 l(t('edit'), 'admin/settings/language/edit/'. $key) . (($key != 'en' && $key != $default->language) ? ' '. l(t('delete'), 'admin/settings/language/delete/'. $key) : '') 90 ); 91 } 92 } 93 $header = array(array('data' => t('Enabled')), array('data' => t('Code')), array('data' => t('English name')), array('data' => t('Native name')), array('data' => t('Direction')), array('data' => t('Default')), array('data' => t('Weight')), array('data' => t('Operations'))); 94 $output = theme('table', $header, $rows); 95 $output .= drupal_render($form); 96 97 return $output; 98 } 99 100 /** 101 * Process language overview form submissions, updating existing languages. 102 */ 103 function locale_languages_overview_form_submit($form, &$form_state) { 104 $languages = language_list(); 105 $default = language_default(); 106 $enabled_count = 0; 107 foreach ($languages as $langcode => $language) { 108 if ($form_state['values']['site_default'] == $langcode || $default->language == $langcode) { 109 // Automatically enable the default language and the language 110 // which was default previously (because we will not get the 111 // value from that disabled checkox). 112 $form_state['values']['enabled'][$langcode] = 1; 113 } 114 if ($form_state['values']['enabled'][$langcode]) { 115 $enabled_count++; 116 $language->enabled = 1; 117 } 118 else { 119 $language->enabled = 0; 120 } 121 $language->weight = $form_state['values']['weight'][$langcode]; 122 db_query("UPDATE {languages} SET enabled = %d, weight = %d WHERE language = '%s'", $language->enabled, $language->weight, $langcode); 123 $languages[$langcode] = $language; 124 } 125 drupal_set_message(t('Configuration saved.')); 126 variable_set('language_default', $languages[$form_state['values']['site_default']]); 127 variable_set('language_count', $enabled_count); 128 129 // Changing the language settings impacts the interface. 130 cache_clear_all('*', 'cache_page', TRUE); 131 132 $form_state['redirect'] = 'admin/settings/language'; 133 return; 134 } 135 /** 136 * @} End of "locale-language-overview" 137 */ 138 139 /** 140 * @defgroup locale-language-add-edit Language addition and editing functionality 141 * @{ 142 */ 143 144 /** 145 * User interface for the language addition screen. 146 */ 147 function locale_languages_add_screen() { 148 $output = drupal_get_form('locale_languages_predefined_form'); 149 $output .= drupal_get_form('locale_languages_custom_form'); 150 return $output; 151 } 152 153 /** 154 * Predefined language setup form. 155 */ 156 function locale_languages_predefined_form() { 157 $predefined = _locale_prepare_predefined_list(); 158 $form = array(); 159 $form['language list'] = array('#type' => 'fieldset', 160 '#title' => t('Predefined language'), 161 '#collapsible' => TRUE, 162 ); 163 $form['language list']['langcode'] = array('#type' => 'select', 164 '#title' => t('Language name'), 165 '#default_value' => key($predefined), 166 '#options' => $predefined, 167 '#description' => t('Select the desired language and click the <em>Add language</em> button. (Use the <em>Custom language</em> options if your desired language does not appear in this list.)'), 168 ); 169 $form['language list']['submit'] = array('#type' => 'submit', '#value' => t('Add language')); 170 return $form; 171 } 172 173 /** 174 * Custom language addition form. 175 */ 176 function locale_languages_custom_form() { 177 $form = array(); 178 $form['custom language'] = array('#type' => 'fieldset', 179 '#title' => t('Custom language'), 180 '#collapsible' => TRUE, 181 '#collapsed' => TRUE, 182 ); 183 _locale_languages_common_controls($form['custom language']); 184 $form['custom language']['submit'] = array( 185 '#type' => 'submit', 186 '#value' => t('Add custom language') 187 ); 188 // Reuse the validation and submit functions of the predefined language setup form. 189 $form['#submit'][] = 'locale_languages_predefined_form_submit'; 190 $form['#validate'][] = 'locale_languages_predefined_form_validate'; 191 return $form; 192 } 193 194 /** 195 * Editing screen for a particular language. 196 * 197 * @param $langcode 198 * Language code of the language to edit. 199 */ 200 function locale_languages_edit_form(&$form_state, $langcode) { 201 if ($language = db_fetch_object(db_query("SELECT * FROM {languages} WHERE language = '%s'", $langcode))) { 202 $form = array(); 203 _locale_languages_common_controls($form, $language); 204 $form['submit'] = array( 205 '#type' => 'submit', 206 '#value' => t('Save language') 207 ); 208 $form['#submit'][] = 'locale_languages_edit_form_submit'; 209 $form['#validate'][] = 'locale_languages_edit_form_validate'; 210 return $form; 211 } 212 else { 213 drupal_not_found(); 214 } 215 } 216 217 /** 218 * Common elements of the language addition and editing form. 219 * 220 * @param $form 221 * A parent form item (or empty array) to add items below. 222 * @param $language 223 * Language object to edit. 224 */ 225 function _locale_languages_common_controls(&$form, $language = NULL) { 226 if (!is_object($language)) { 227 $language = new stdClass(); 228 } 229 if (isset($language->language)) { 230 $form['langcode_view'] = array( 231 '#type' => 'item', 232 '#title' => t('Language code'), 233 '#value' => $language->language 234 ); 235 $form['langcode'] = array( 236 '#type' => 'value', 237 '#value' => $language->language 238 ); 239 } 240 else { 241 $form['langcode'] = array('#type' => 'textfield', 242 '#title' => t('Language code'), 243 '#size' => 12, 244 '#maxlength' => 60, 245 '#required' => TRUE, 246 '#default_value' => @$language->language, 247 '#disabled' => (isset($language->language)), 248 '#description' => t('<a href="@rfc4646">RFC 4646</a> compliant language identifier. Language codes typically use a country code, and optionally, a script or regional variant name. <em>Examples: "en", "en-US" and "zh-Hant".</em>', array('@rfc4646' => 'http://www.ietf.org/rfc/rfc4646.txt')), 249 ); 250 } 251 $form['name'] = array('#type' => 'textfield', 252 '#title' => t('Language name in English'), 253 '#maxlength' => 64, 254 '#default_value' => @$language->name, 255 '#required' => TRUE, 256 '#description' => t('Name of the language in English. Will be available for translation in all languages.'), 257 ); 258 $form['native'] = array('#type' => 'textfield', 259 '#title' => t('Native language name'), 260 '#maxlength' => 64, 261 '#default_value' => @$language->native, 262 '#required' => TRUE, 263 '#description' => t('Name of the language in the language being added.'), 264 ); 265 $form['prefix'] = array('#type' => 'textfield', 266 '#title' => t('Path prefix'), 267 '#maxlength' => 64, 268 '#default_value' => @$language->prefix, 269 '#description' => t('Language code or other custom string for pattern matching within the path. With language negotiation set to <em>Path prefix only</em> or <em>Path prefix with language fallback</em>, this site is presented in this language when the Path prefix value matches an element in the path. For the default language, this value may be left blank. <strong>Modifying this value will break existing URLs and should be used with caution in a production environment.</strong> <em>Example: Specifying "deutsch" as the path prefix for German results in URLs in the form "www.example.com/deutsch/node".</em>') 270 ); 271 $form['domain'] = array('#type' => 'textfield', 272 '#title' => t('Language domain'), 273 '#maxlength' => 128, 274 '#default_value' => @$language->domain, 275 '#description' => t('Language-specific URL, with protocol. With language negotiation set to <em>Domain name only</em>, the site is presented in this language when the URL accessing the site references this domain. For the default language, this value may be left blank. <strong>This value must include a protocol as part of the string.</strong> <em>Example: Specifying "http://example.de" or "http://de.example.com" as language domains for German results in URLs in the forms "http://example.de/node" and "http://de.example.com/node", respectively.</em>'), 276 ); 277 $form['direction'] = array('#type' => 'radios', 278 '#title' => t('Direction'), 279 '#required' => TRUE, 280 '#description' => t('Direction that text in this language is presented.'), 281 '#default_value' => @$language->direction, 282 '#options' => array(LANGUAGE_LTR => t('Left to right'), LANGUAGE_RTL => t('Right to left')) 283 ); 284 return $form; 285 } 286 287 /** 288 * Validate the language addition form. 289 */ 290 function locale_languages_predefined_form_validate($form, &$form_state) { 291 $langcode = $form_state['values']['langcode']; 292 293 if ($duplicate = db_result(db_query("SELECT COUNT(*) FROM {languages} WHERE language = '%s'", $langcode)) != 0) { 294 form_set_error('langcode', t('The language %language (%code) already exists.', array('%language' => $form_state['values']['name'], '%code' => $langcode))); 295 } 296 297 if (!isset($form_state['values']['name'])) { 298 // Predefined language selection. 299 $predefined = _locale_get_predefined_list(); 300 if (!isset($predefined[$langcode])) { 301 form_set_error('langcode', t('Invalid language code.')); 302 } 303 } 304 else { 305 // Reuse the editing form validation routine if we add a custom language. 306 locale_languages_edit_form_validate($form, $form_state); 307 } 308 } 309 310 /** 311 * Process the language addition form submission. 312 */ 313 function locale_languages_predefined_form_submit($form, &$form_state) { 314 $langcode = $form_state['values']['langcode']; 315 if (isset($form_state['values']['name'])) { 316 // Custom language form. 317 locale_add_language($langcode, $form_state['values']['name'], $form_state['values']['native'], $form_state['values']['direction'], $form_state['values']['domain'], $form_state['values']['prefix']); 318 drupal_set_message(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => t($form_state['values']['name']), '@locale-help' => url('admin/help/locale')))); 319 } 320 else { 321 // Predefined language selection. 322 $predefined = _locale_get_predefined_list(); 323 locale_add_language($langcode); 324 drupal_set_message(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => t($predefined[$langcode][0]), '@locale-help' => url('admin/help/locale')))); 325 } 326 327 // See if we have language files to import for the newly added 328 // language, collect and import them. 329 if ($batch = locale_batch_by_language($langcode, '_locale_batch_language_finished')) { 330 batch_set($batch); 331 } 332 333 $form_state['redirect'] = 'admin/settings/language'; 334 return; 335 } 336 337 /** 338 * Validate the language editing form. Reused for custom language addition too. 339 */ 340 function locale_languages_edit_form_validate($form, &$form_state) { 341 // Validate that the name, native, and langcode variables are safe. 342 if (preg_match('/["<>\']/', $form_state['values']['langcode'])) { 343 form_set_error('langcode', t('The characters <, >, " and \' are not allowed in the language code field.')); 344 } 345 if (preg_match('/["<>\']/', $form_state['values']['name'])) { 346 form_set_error('name', t('The characters <, >, " and \' are not allowed in the language name in English field.')); 347 } 348 if (preg_match('/["<>\']/', $form_state['values']['native'])) { 349 form_set_error('native', t('The characters <, >, " and \' are not allowed in the native language name field.')); 350 } 351 352 if (!empty($form_state['values']['domain']) && !empty($form_state['values']['prefix'])) { 353 form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.')); 354 } 355 if (!empty($form_state['values']['domain']) && $duplicate = db_fetch_object(db_query("SELECT language FROM {languages} WHERE domain = '%s' AND language != '%s'", $form_state['values']['domain'], $form_state['values']['langcode']))) { 356 form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate->language))); 357 } 358 if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) { 359 form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.')); 360 } 361 if (!empty($form_state['values']['prefix']) && $duplicate = db_fetch_object(db_query("SELECT language FROM {languages} WHERE prefix = '%s' AND language != '%s'", $form_state['values']['prefix'], $form_state['values']['langcode']))) { 362 form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate->language))); 363 } 364 } 365 366 /** 367 * Process the language editing form submission. 368 */ 369 function locale_languages_edit_form_submit($form, &$form_state) { 370 db_query("UPDATE {languages} SET name = '%s', native = '%s', domain = '%s', prefix = '%s', direction = %d WHERE language = '%s'", $form_state['values']['name'], $form_state['values']['native'], $form_state['values']['domain'], $form_state['values']['prefix'], $form_state['values']['direction'], $form_state['values']['langcode']); 371 $default = language_default(); 372 if ($default->language == $form_state['values']['langcode']) { 373 $properties = array('name', 'native', 'direction', 'enabled', 'plurals', 'formula', 'domain', 'prefix', 'weight'); 374 foreach ($properties as $keyname) { 375 if (isset($form_state['values'][$keyname])) { 376 $default->$keyname = $form_state['values'][$keyname]; 377 } 378 } 379 variable_set('language_default', $default); 380 } 381 $form_state['redirect'] = 'admin/settings/language'; 382 return; 383 } 384 /** 385 * @} End of "locale-language-add-edit" 386 */ 387 388 /** 389 * @defgroup locale-language-delete Language deletion functionality 390 * @{ 391 */ 392 393 /** 394 * User interface for the language deletion confirmation screen. 395 */ 396 function locale_languages_delete_form(&$form_state, $langcode) { 397 398 // Do not allow deletion of English locale. 399 if ($langcode == 'en') { 400 drupal_set_message(t('The English language cannot be deleted.')); 401 drupal_goto('admin/settings/language'); 402 } 403 404 if (language_default('language') == $langcode) { 405 drupal_set_message(t('The default language cannot be deleted.')); 406 drupal_goto('admin/settings/language'); 407 } 408 409 // For other languages, warn user that data loss is ahead. 410 $languages = language_list(); 411 412 if (!isset($languages[$langcode])) { 413 drupal_not_found(); 414 } 415 else { 416 $form['langcode'] = array('#type' => 'value', '#value' => $langcode); 417 return confirm_form($form, t('Are you sure you want to delete the language %name?', array('%name' => t($languages[$langcode]->name))), 'admin/settings/language', t('Deleting a language will remove all interface translations associated with it, and posts in this language will be set to be language neutral. This action cannot be undone.'), t('Delete'), t('Cancel')); 418 } 419 } 420 421 /** 422 * Process language deletion submissions. 423 */ 424 function locale_languages_delete_form_submit($form, &$form_state) { 425 $languages = language_list(); 426 if (isset($languages[$form_state['values']['langcode']])) { 427 // Remove translations first. 428 db_query("DELETE FROM {locales_target} WHERE language = '%s'", $form_state['values']['langcode']); 429 cache_clear_all('locale:'. $form_state['values']['langcode'], 'cache'); 430 // With no translations, this removes existing JavaScript translations file. 431 _locale_rebuild_js($form_state['values']['langcode']); 432 // Remove the language. 433 db_query("DELETE FROM {languages} WHERE language = '%s'", $form_state['values']['langcode']); 434 db_query("UPDATE {node} SET language = '' WHERE language = '%s'", $form_state['values']['langcode']); 435 $variables = array('%locale' => $languages[$form_state['values']['langcode']]->name); 436 drupal_set_message(t('The language %locale has been removed.', $variables)); 437 watchdog('locale', 'The language %locale has been removed.', $variables); 438 } 439 440 // Changing the language settings impacts the interface: 441 cache_clear_all('*', 'cache_page', TRUE); 442 443 $form_state['redirect'] = 'admin/settings/language'; 444 return; 445 } 446 /** 447 * @} End of "locale-language-add-edit" 448 */ 449 450 /** 451 * @defgroup locale-languages-negotiation Language negotiation options screen 452 * @{ 453 */ 454 455 /** 456 * Setting for language negotiation options 457 */ 458 function locale_languages_configure_form() { 459 $form['language_negotiation'] = array( 460 '#title' => t('Language negotiation'), 461 '#type' => 'radios', 462 '#options' => array( 463 LANGUAGE_NEGOTIATION_NONE => t('None.'), 464 LANGUAGE_NEGOTIATION_PATH_DEFAULT => t('Path prefix only.'), 465 LANGUAGE_NEGOTIATION_PATH => t('Path prefix with language fallback.'), 466 LANGUAGE_NEGOTIATION_DOMAIN => t('Domain name only.')), 467 '#default_value' => variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE), 468 '#description' => t("Select the mechanism used to determine your site's presentation language. <strong>Modifying this setting may break all incoming URLs and should be used with caution in a production environment.</strong>") 469 ); 470 $form['submit'] = array( 471 '#type' => 'submit', 472 '#value' => t('Save settings') 473 ); 474 return $form; 475 } 476 477 /** 478 * Submit function for language negotiation settings. 479 */ 480 function locale_languages_configure_form_submit($form, &$form_state) { 481 variable_set('language_negotiation', $form_state['values']['language_negotiation']); 482 drupal_set_message(t('Language negotiation configuration saved.')); 483 $form_state['redirect'] = 'admin/settings/language'; 484 return; 485 } 486 /** 487 * @} End of "locale-languages-negotiation" 488 */ 489 490 /** 491 * @defgroup locale-translate-overview Translation overview screen. 492 * @{ 493 */ 494 495 /** 496 * Overview screen for translations. 497 */ 498 function locale_translate_overview_screen() { 499 $languages = language_list('language', TRUE); 500 $groups = module_invoke_all('locale', 'groups'); 501 502 // Build headers with all groups in order. 503 $headers = array_merge(array(t('Language')), array_values($groups)); 504 505 // Collect summaries of all source strings in all groups. 506 $sums = db_query("SELECT COUNT(*) AS strings, textgroup FROM {locales_source} GROUP BY textgroup"); 507 $groupsums = array(); 508 while ($group = db_fetch_object($sums)) { 509 $groupsums[$group->textgroup] = $group->strings; 510 } 511 512 // Set up overview table with default values, ensuring common order for values. 513 $rows = array(); 514 foreach ($languages as $langcode => $language) { 515 $rows[$langcode] = array('name' => ($langcode == 'en' ? t('English (built-in)') : t($language->name))); 516 foreach ($groups as $group => $name) { 517 $rows[$langcode][$group] = ($langcode == 'en' ? t('n/a') : '0/'. (isset($groupsums[$group]) ? $groupsums[$group] : 0) .' (0%)'); 518 } 519 } 520 521 // Languages with at least one record in the locale table. 522 $translations = db_query("SELECT COUNT(*) AS translation, t.language, s.textgroup FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY textgroup, language"); 523 while ($data = db_fetch_object($translations)) { 524 $ratio = (!empty($groupsums[$data->textgroup]) && $data->translation > 0) ? round(($data->translation/$groupsums[$data->textgroup])*100., 2) : 0; 525 $rows[$data->language][$data->textgroup] = $data->translation .'/'. $groupsums[$data->textgroup] ." ($ratio%)"; 526 } 527 528 return theme('table', $headers, $rows); 529 } 530 /** 531 * @} End of "locale-translate-overview" 532 */ 533 534 /** 535 * @defgroup locale-translate-seek Translation search screen. 536 * @{ 537 */ 538 539 /** 540 * String search screen. 541 */ 542 function locale_translate_seek_screen() { 543 $output = _locale_translate_seek(); 544 $output .= drupal_get_form('locale_translate_seek_form'); 545 return $output; 546 } 547 548 /** 549 * User interface for the string search screen. 550 */ 551 function locale_translate_seek_form() { 552 // Get all languages, except English 553 $raw_languages = locale_language_list('name', TRUE); 554 unset($raw_languages['en']); 555 // Sanitize the values to be used in radios. 556 $languages = array(); 557 foreach ($raw_languages as $key => $value) { 558 $languages[check_plain($key)] = check_plain($value); 559 } 560 561 // Present edit form preserving previous user settings 562 $query = _locale_translate_seek_query(); 563 $form = array(); 564 $form['search'] = array('#type' => 'fieldset', 565 '#title' => t('Search'), 566 ); 567 $form['search']['string'] = array('#type' => 'textfield', 568 '#title' => t('String contains'), 569 '#default_value' => @$query['string'], 570 '#description' => t('Leave blank to show all strings. The search is case sensitive.'), 571 ); 572 $form['search']['language'] = array( 573 // Change type of form widget if more the 5 options will 574 // be present (2 of the options are added below). 575 '#type' => (count($languages) <= 3 ? 'radios' : 'select'), 576 '#title' => t('Language'), 577 '#default_value' => (!empty($query['language']) ? $query['language'] : 'all'), 578 '#options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages), 579 ); 580 $form['search']['translation'] = array('#type' => 'radios', 581 '#title' => t('Search in'), 582 '#default_value' => (!empty($query['translation']) ? $query['translation'] : 'all'), 583 '#options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')), 584 ); 585 $groups = module_invoke_all('locale', 'groups'); 586 $form['search']['group'] = array('#type' => 'radios', 587 '#title' => t('Limit search to'), 588 '#default_value' => (!empty($query['group']) ? $query['group'] : 'all'), 589 '#options' => array_merge(array('all' => t('All text groups')), $groups), 590 ); 591 592 $form['search']['submit'] = array('#type' => 'submit', '#value' => t('Search')); 593 $form['#redirect'] = FALSE; 594 595 return $form; 596 } 597 /** 598 * @} End of "locale-translate-seek" 599 */ 600 601 /** 602 * @defgroup locale-translate-import Translation import screen. 603 * @{ 604 */ 605 606 /** 607 * User interface for the translation import screen. 608 */ 609 function locale_translate_import_form() { 610 // Get all languages, except English 611 $names = locale_language_list('name', TRUE); 612 unset($names['en']); 613 614 if (!count($names)) { 615 $languages = _locale_prepare_predefined_list(); 616 $default = array_shift(array_keys($languages)); 617 } 618 else { 619 $languages = array( 620 t('Already added languages') => $names, 621 t('Languages not yet added') => _locale_prepare_predefined_list() 622 ); 623 $default = array_shift(array_keys($names)); 624 } 625 626 $form = array(); 627 $form['import'] = array('#type' => 'fieldset', 628 '#title' => t('Import translation'), 629 ); 630 $form['import']['file'] = array('#type' => 'file', 631 '#title' => t('Language file'), 632 '#size' => 50, 633 '#description' => t('A Gettext Portable Object (<em>.po</em>) file.'), 634 ); 635 $form['import']['langcode'] = array('#type' => 'select', 636 '#title' => t('Import into'), 637 '#options' => $languages, 638 '#default_value' => $default, 639 '#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, it will be added.'), 640 ); 641 $form['import']['group'] = array('#type' => 'radios', 642 '#title' => t('Text group'), 643 '#default_value' => 'default', 644 '#options' => module_invoke_all('locale', 'groups'), 645 '#description' => t('Imported translations will be added to this text group.'), 646 ); 647 $form['import']['mode'] = array('#type' => 'radios', 648 '#title' => t('Mode'), 649 '#default_value' => LOCALE_IMPORT_KEEP, 650 '#options' => array( 651 LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added'), 652 LOCALE_IMPORT_KEEP => t('Existing strings are kept, only new strings are added') 653 ), 654 ); 655 $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import')); 656 $form['#attributes']['enctype'] = 'multipart/form-data'; 657 658 return $form; 659 } 660 661 /** 662 * Process the locale import form submission. 663 */ 664 function locale_translate_import_form_submit($form, &$form_state) { 665 // Ensure we have the file uploaded 666 if ($file = file_save_upload('file')) { 667 668 // Add language, if not yet supported 669 $languages = language_list('language', TRUE); 670 $langcode = $form_state['values']['langcode']; 671 if (!isset($languages[$langcode])) { 672 $predefined = _locale_get_predefined_list(); 673 locale_add_language($langcode); 674 drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0])))); 675 } 676 677 // Now import strings into the language 678 if ($ret = _locale_import_po($file, $langcode, $form_state['values']['mode'], $form_state['values']['group']) == FALSE) { 679 $variables = array('%filename' => $file->filename); 680 drupal_set_message(t('The translation import of %filename failed.', $variables), 'error'); 681 watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR); 682 } 683 } 684 else { 685 drupal_set_message(t('File to import not found.'), 'error'); 686 $form_state['redirect'] = 'admin/build/translate/import'; 687 return; 688 } 689 690 $form_state['redirect'] = 'admin/build/translate'; 691 return; 692 } 693 /** 694 * @} End of "locale-translate-import" 695 */ 696 697 /** 698 * @defgroup locale-translate-export Translation export screen. 699 * @{ 700 */ 701 702 /** 703 * User interface for the translation export screen. 704 */ 705 function locale_translate_export_screen() { 706 // Get all languages, except English 707 $names = locale_language_list('name', TRUE); 708 unset($names['en']); 709 $output = ''; 710 // Offer translation export if any language is set up. 711 if (count($names)) { 712 $output = drupal_get_form('locale_translate_export_po_form', $names); 713 } 714 $output .= drupal_get_form('locale_translate_export_pot_form'); 715 return $output; 716 } 717 718 /** 719 * Form to export PO files for the languages provided. 720 * 721 * @param $names 722 * An associate array with localized language names 723 */ 724 function locale_translate_export_po_form(&$form_state, $names) { 725 $form['export'] = array('#type' => 'fieldset', 726 '#title' => t('Export translation'), 727 '#collapsible' => TRUE, 728 ); 729 $form['export']['langcode'] = array('#type' => 'select', 730 '#title' => t('Language name'), 731 '#options' => $names, 732 '#description' => t('Select the language to export in Gettext Portable Object (<em>.po</em>) format.'), 733 ); 734 $form['export']['group'] = array('#type' => 'radios', 735 '#title' => t('Text group'), 736 '#default_value' => 'default', 737 '#options' => module_invoke_all('locale', 'groups'), 738 ); 739 $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export')); 740 return $form; 741 } 742 743 /** 744 * Translation template export form. 745 */ 746 function locale_translate_export_pot_form() { 747 // Complete template export of the strings 748 $form['export'] = array('#type' => 'fieldset', 749 '#title' => t('Export template'), 750 '#collapsible' => TRUE, 751 '#description' => t('Generate a Gettext Portable Object Template (<em>.pot</em>) file with all strings from the Drupal locale database.'), 752 ); 753 $form['export']['group'] = array('#type' => 'radios', 754 '#title' => t('Text group'), 755 '#default_value' => 'default', 756 '#options' => module_invoke_all('locale', 'groups'), 757 ); 758 $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export')); 759 // Reuse PO export submission callback. 760 $form['#submit'][] = 'locale_translate_export_po_form_submit'; 761 $form['#validate'][] = 'locale_translate_export_po_form_validate'; 762 return $form; 763 } 764 765 /** 766 * Process a translation (or template) export form submission. 767 */ 768 function locale_translate_export_po_form_submit($form, &$form_state) { 769 // If template is required, language code is not given. 770 $language = NULL; 771 if (isset($form_state['values']['langcode'])) { 772 $languages = language_list(); 773 $language = $languages[$form_state['values']['langcode']]; 774 } 775 _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $form_state['values']['group']))); 776 } 777 /** 778 * @} End of "locale-translate-export" 779 */ 780 781 /** 782 * @defgroup locale-translate-edit Translation text editing 783 * @{ 784 */ 785 786 /** 787 * User interface for string editing. 788 */ 789 function locale_translate_edit_form(&$form_state, $lid) { 790 // Fetch source string, if possible. 791 $source = db_fetch_object(db_query('SELECT source, textgroup, location FROM {locales_source} WHERE lid = %d', $lid)); 792 if (!$source) { 793 drupal_set_message(t('String not found.'), 'error'); 794 drupal_goto('admin/build/translate/search'); 795 } 796 797 // Add original text to the top and some values for form altering. 798 $form = array( 799 'original' => array( 800 '#type' => 'item', 801 '#title' => t('Original text'), 802 '#value' => check_plain(wordwrap($source->source, 0)), 803 ), 804 'lid' => array( 805 '#type' => 'value', 806 '#value' => $lid 807 ), 808 'textgroup' => array( 809 '#type' => 'value', 810 '#value' => $source->textgroup, 811 ), 812 'location' => array( 813 '#type' => 'value', 814 '#value' => $source->location 815 ), 816 ); 817 818 // Include default form controls with empty values for all languages. 819 // This ensures that the languages are always in the same order in forms. 820 $languages = language_list(); 821 $default = language_default(); 822 // We don't need the default language value, that value is in $source. 823 $omit = $source->textgroup == 'default' ? 'en' : $default->language; 824 unset($languages[($omit)]); 825 $form['translations'] = array('#tree' => TRUE); 826 // Approximate the number of rows to use in the default textarea. 827 $rows = min(ceil(str_word_count($source->source) / 12), 10); 828 foreach ($languages as $langcode => $language) { 829 $form['translations'][$langcode] = array( 830 '#type' => 'textarea', 831 '#title' => t($language->name), 832 '#rows' => $rows, 833 '#default_value' => '', 834 ); 835 } 836 837 // Fetch translations and fill in default values in the form. 838 $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = %d AND language != '%s'", $lid, $omit); 839 while ($translation = db_fetch_object($result)) { 840 $form['translations'][$translation->language]['#default_value'] = $translation->translation; 841 } 842 843 $form['submit'] = array('#type' => 'submit', '#value' => t('Save translations')); 844 return $form; 845 } 846 847 /** 848 * Check that a string is safe to be added or imported as a translation. 849 * 850 * This test can be used to detect possibly bad translation strings. It should 851 * not have any false positives. But it is only a test, not a transformation, 852 * as it destroys valid HTML. We cannot reliably filter translation strings 853 * on inport becuase some strings are irreversibly corrupted. For example, 854 * a & in the translation would get encoded to &amp; by filter_xss() 855 * before being put in the database, and thus would be displayed incorrectly. 856 * 857 * The allowed tag list is like filter_xss_admin(), but omitting div and img as 858 * not needed for translation and likely to cause layout issues (div) or a 859 * possible attack vector (img). 860 */ 861 function locale_string_is_safe($string) { 862 return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var'))); 863 } 864 865 /** 866 * Validate string editing form submissions. 867 */ 868 function locale_translate_edit_form_validate($form, &$form_state) { 869 // Locale string check is needed for default textgroup only. 870 $safe_check_needed = $form_state['values']['textgroup'] == 'default'; 871 foreach ($form_state['values']['translations'] as $key => $value) { 872 if ($safe_check_needed && !locale_string_is_safe($value)) { 873 form_set_error('translations', t('The submitted string contains disallowed HTML: %string', array('%string' => $value))); 874 watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), WATCHDOG_WARNING); 875 } 876 } 877 } 878 879 /** 880 * Process string editing form submissions. 881 * 882 * Saves all translations of one string submitted from a form. 883 */ 884 function locale_translate_edit_form_submit($form, &$form_state) { 885 $lid = $form_state['values']['lid']; 886 foreach ($form_state['values']['translations'] as $key => $value) { 887 $translation = db_result(db_query("SELECT translation FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $key)); 888 if (!empty($value)) { 889 // Only update or insert if we have a value to use. 890 if (!empty($translation)) { 891 db_query("UPDATE {locales_target} SET translation = '%s' WHERE lid = %d AND language = '%s'", $value, $lid, $key); 892 } 893 else { 894 db_query("INSERT INTO {locales_target} (lid, translation, language) VALUES (%d, '%s', '%s')", $lid, $value, $key); 895 } 896 } 897 elseif (!empty($translation)) { 898 // Empty translation entered: remove existing entry from database. 899 db_query("DELETE FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $key); 900 } 901 902 // Force JavaScript translation file recreation for this language. 903 _locale_invalidate_js($key); 904 } 905 906 drupal_set_message(t('The string has been saved.')); 907 908 // Clear locale cache. 909 _locale_invalidate_js(); 910 cache_clear_all('locale:', 'cache', TRUE); 911 912 $form_state['redirect'] = 'admin/build/translate/search'; 913 return; 914 } 915 /** 916 * @} End of "locale-translate-edit" 917 */ 918 919 /** 920 * @defgroup locale-translate-delete Translation delete interface. 921 * @{ 922 */ 923 924 /** 925 * String deletion confirmation page. 926 */ 927 function locale_translate_delete_page($lid) { 928 if ($source = db_fetch_object(db_query('SELECT * FROM {locales_source} WHERE lid = %d', $lid))) { 929 return drupal_get_form('locale_translate_delete_form', $source); 930 } 931 else { 932 return drupal_not_found(); 933 } 934 } 935 936 /** 937 * User interface for the string deletion confirmation screen. 938 */ 939 function locale_translate_delete_form(&$form_state, $source) { 940 $form['lid'] = array('#type' => 'value', '#value' => $source->lid); 941 return confirm_form($form, t('Are you sure you want to delete the string "%source"?', array('%source' => $source->source)), 'admin/build/translate/search', t('Deleting the string will remove all translations of this string in all languages. This action cannot be undone.'), t('Delete'), t('Cancel')); 942 } 943 944 /** 945 * Process string deletion submissions. 946 */ 947 function locale_translate_delete_form_submit($form, &$form_state) { 948 db_query('DELETE FROM {locales_source} WHERE lid = %d', $form_state['values']['lid']); 949 db_query('DELETE FROM {locales_target} WHERE lid = %d', $form_state['values']['lid']); 950 // Force JavaScript translation file recreation for all languages. 951 _locale_invalidate_js(); 952 cache_clear_all('locale:', 'cache', TRUE); 953 drupal_set_message(t('The string has been removed.')); 954 $form_state['redirect'] = 'admin/build/translate/search'; 955 } 956 /** 957 * @} End of "locale-translate-delete" 958 */ 959 960 /** 961 * @defgroup locale-api-add Language addition API. 962 * @{ 963 */ 964 965 /** 966 * API function to add a language. 967 * 968 * @param $langcode 969 * Language code. 970 * @param $name 971 * English name of the language 972 * @param $native 973 * Native name of the language 974 * @param $direction 975 * LANGUAGE_LTR or LANGUAGE_RTL 976 * @param $domain 977 * Optional custom domain name with protocol, without 978 * trailing slash (eg. http://de.example.com). 979 * @param $prefix 980 * Optional path prefix for the language. Defaults to the 981 * language code if omitted. 982 * @param $enabled 983 * Optionally TRUE to enable the language when created or FALSE to disable. 984 * @param $default 985 * Optionally set this language to be the default. 986 */ 987 function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) { 988 // Default prefix on language code. 989 if (empty($prefix)) { 990 $prefix = $langcode; 991 } 992 993 // If name was not set, we add a predefined language. 994 if (!isset($name)) { 995 $predefined = _locale_get_predefined_list(); 996 $name = $predefined[$langcode][0]; 997 $native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0]; 998 $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR; 999 } 1000 1001 db_query("INSERT INTO {languages} (language, name, native, direction, domain, prefix, enabled) VALUES ('%s', '%s', '%s', %d, '%s', '%s', %d)", $langcode, $name, $native, $direction, $domain, $prefix, $enabled); 1002 1003 // Only set it as default if enabled. 1004 if ($enabled && $default) { 1005 variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => '')); 1006 } 1007 1008 if ($enabled) { 1009 // Increment enabled language count if we are adding an enabled language. 1010 variable_set('language_count', variable_get('language_count', 1) + 1); 1011 } 1012 1013 // Force JavaScript translation file creation for the newly added language. 1014 _locale_invalidate_js($langcode); 1015 1016 watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode)); 1017 } 1018 /** 1019 * @} End of "locale-api-add" 1020 */ 1021 1022 /** 1023 * @defgroup locale-api-import Translation import API. 1024 * @{ 1025 */ 1026 1027 /** 1028 * Parses Gettext Portable Object file information and inserts into database 1029 * 1030 * @param $file 1031 * Drupal file object corresponding to the PO file to import 1032 * @param $langcode 1033 * Language code 1034 * @param $mode 1035 * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE 1036 * @param $group 1037 * Text group to import PO file into (eg. 'default' for interface translations) 1038 */ 1039 function _locale_import_po($file, $langcode, $mode, $group = NULL) { 1040 // Try to allocate enough time to parse and import the data. 1041 if (function_exists('set_time_limit')) { 1042 @set_time_limit(240); 1043 } 1044 1045 // Check if we have the language already in the database. 1046 if (!db_fetch_object(db_query("SELECT language FROM {languages} WHERE language = '%s'", $langcode))) { 1047 drupal_set_message(t('The language selected for import is not supported.'), 'error'); 1048 return FALSE; 1049 } 1050 1051 // Get strings from file (returns on failure after a partial import, or on success) 1052 $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group); 1053 if ($status === FALSE) { 1054 // Error messages are set in _locale_import_read_po(). 1055 return FALSE; 1056 } 1057 1058 // Get status information on import process. 1059 list($headerdone, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report'); 1060 1061 if (!$headerdone) { 1062 drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error'); 1063 } 1064 1065 // Clear cache and force refresh of JavaScript translations. 1066 _locale_invalidate_js($langcode); 1067 cache_clear_all('locale:', 'cache', TRUE); 1068 1069 // Rebuild the menu, strings may have changed. 1070 menu_rebuild(); 1071 1072 drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); 1073 watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes)); 1074 if ($skips) { 1075 $skip_message = format_plural($skips, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.'); 1076 drupal_set_message($skip_message); 1077 watchdog('locale', $skip_message, NULL, WATCHDOG_WARNING); 1078 } 1079 return TRUE; 1080 } 1081 1082 /** 1083 * Parses Gettext Portable Object file into an array 1084 * 1085 * @param $op 1086 * Storage operation type: db-store or mem-store 1087 * @param $file 1088 * Drupal file object corresponding to the PO file to import 1089 * @param $mode 1090 * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE 1091 * @param $lang 1092 * Language code 1093 * @param $group 1094 * Text group to import PO file into (eg. 'default' for interface translations) 1095 */ 1096 function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') { 1097 1098 $fd = fopen($file->filepath, "rb"); // File will get closed by PHP on return 1099 if (!$fd) { 1100 _locale_import_message('The translation import failed, because the file %filename could not be read.', $file); 1101 return FALSE; 1102 } 1103 1104 $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR 1105 $current = array(); // Current entry being read 1106 $plural = 0; // Current plural form 1107 $lineno = 0; // Current line 1108 1109 while (!feof($fd)) { 1110 $line = fgets($fd, 10*1024); // A line should not be this long 1111 if ($lineno == 0) { 1112 // The first line might come with a UTF-8 BOM, which should be removed. 1113 $line = str_replace("\xEF\xBB\xBF", '', $line); 1114 } 1115 $lineno++; 1116 $line = trim(strtr($line, array("\\\n" => ""))); 1117 1118 if (!strncmp("#", $line, 1)) { // A comment 1119 if ($context == "COMMENT") { // Already in comment context: add 1120 $current["#"][] = substr($line, 1); 1121 } 1122 elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one 1123 _locale_import_one_string($op, $current, $mode, $lang, $file, $group); 1124 $current = array(); 1125 $current["#"][] = substr($line, 1); 1126 $context = "COMMENT"; 1127 } 1128 else { // Parse error 1129 _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno); 1130 return FALSE; 1131 } 1132 } 1133 elseif (!strncmp("msgid_plural", $line, 12)) { 1134 if ($context != "MSGID") { // Must be plural form for current entry 1135 _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno); 1136 return FALSE; 1137 } 1138 $line = trim(substr($line, 12)); 1139 $quoted = _locale_import_parse_quoted($line); 1140 if ($quoted === FALSE) { 1141 _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); 1142 return FALSE; 1143 } 1144 $current["msgid"] = $current["msgid"] ."\0". $quoted; 1145 $context = "MSGID_PLURAL"; 1146 } 1147 elseif (!strncmp("msgid", $line, 5)) { 1148 if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one 1149 _locale_import_one_string($op, $current, $mode, $lang, $file, $group); 1150 $current = array(); 1151 } 1152 elseif ($context == "MSGID") { // Already in this context? Parse error 1153 _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno); 1154 return FALSE; 1155 } 1156 $line = trim(substr($line, 5)); 1157 $quoted = _locale_import_parse_quoted($line); 1158 if ($quoted === FALSE) { 1159 _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); 1160 return FALSE; 1161 } 1162 $current["msgid"] = $quoted; 1163 $context = "MSGID"; 1164 } 1165 elseif (!strncmp("msgstr[", $line, 7)) { 1166 if (($context != "MSGID") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgid_plural, or msgstr[] 1167 _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno); 1168 return FALSE; 1169 } 1170 if (strpos($line, "]") === FALSE) { 1171 _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); 1172 return FALSE; 1173 } 1174 $frombracket = strstr($line, "["); 1175 $plural = substr($frombracket, 1, strpos($frombracket, "]") - 1); 1176 $line = trim(strstr($line, " ")); 1177 $quoted = _locale_import_parse_quoted($line); 1178 if ($quoted === FALSE) { 1179 _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); 1180 return FALSE; 1181 } 1182 $current["msgstr"][$plural] = $quoted; 1183 $context = "MSGSTR_ARR"; 1184 } 1185 elseif (!strncmp("msgstr", $line, 6)) { 1186 if ($context != "MSGID") { // Should come just after a msgid block 1187 _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno); 1188 return FALSE; 1189 } 1190 $line = trim(substr($line, 6)); 1191 $quoted = _locale_import_parse_quoted($line); 1192 if ($quoted === FALSE) { 1193 _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); 1194 return FALSE; 1195 } 1196 $current["msgstr"] = $quoted; 1197 $context = "MSGSTR"; 1198 } 1199 elseif ($line != "") { 1200 $quoted = _locale_import_parse_quoted($line); 1201 if ($quoted === FALSE) { 1202 _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); 1203 return FALSE; 1204 } 1205 if (($context == "MSGID") || ($context == "MSGID_PLURAL")) { 1206 $current["msgid"] .= $quoted; 1207 } 1208 elseif ($context == "MSGSTR") { 1209 $current["msgstr"] .= $quoted; 1210 } 1211 elseif ($context == "MSGSTR_ARR") { 1212 $current["msgstr"][$plural] .= $quoted; 1213 } 1214 else { 1215 _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno); 1216 return FALSE; 1217 } 1218 } 1219 } 1220 1221 // End of PO file, flush last entry 1222 if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { 1223 _locale_import_one_string($op, $current, $mode, $lang, $file, $group); 1224 } 1225 elseif ($context != "COMMENT") { 1226 _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno); 1227 return FALSE; 1228 } 1229 1230 } 1231 1232 /** 1233 * Sets an error message occurred during locale file parsing. 1234 * 1235 * @param $message 1236 * The message to be translated 1237 * @param $file 1238 * Drupal file object corresponding to the PO file to import 1239 * @param $lineno 1240 * An optional line number argument 1241 */ 1242 function _locale_import_message($message, $file, $lineno = NULL) { 1243 $vars = array('%filename' => $file->filename); 1244 if (isset($lineno)) { 1245 $vars['%line'] = $lineno; 1246 } 1247 $t = get_t(); 1248 drupal_set_message($t($message, $vars), 'error'); 1249 } 1250 1251 /** 1252 * Imports a string into the database 1253 * 1254 * @param $op 1255 * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report' 1256 * @param $value 1257 * Details of the string stored 1258 * @param $mode 1259 * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE 1260 * @param $lang 1261 * Language to store the string in 1262 * @param $file 1263 * Object representation of file being imported, only required when op is 'db-store' 1264 * @param $group 1265 * Text group to import PO file into (eg. 'default' for interface translations) 1266 */ 1267 function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') { 1268 static $report = array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0); 1269 static $headerdone = FALSE; 1270 static $strings = array(); 1271 1272 switch ($op) { 1273 // Return stored strings 1274 case 'mem-report': 1275 return $strings; 1276 1277 // Store string in memory (only supports single strings) 1278 case 'mem-store': 1279 $strings[$value['msgid']] = $value['msgstr']; 1280 return; 1281 1282 // Called at end of import to inform the user 1283 case 'db-report': 1284 return array($headerdone, $report['additions'], $report['updates'], $report['deletes'], $report['skips']); 1285 1286 // Store the string we got in the database. 1287 case 'db-store': 1288 // We got header information. 1289 if ($value['msgid'] == '') { 1290 $languages = language_list(); 1291 if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) { 1292 // Since we only need to parse the header if we ought to update the 1293 // plural formula, only run this if we don't need to keep existing 1294 // data untouched or if we don't have an existing plural formula. 1295 $header = _locale_import_parse_header($value['msgstr']); 1296 1297 // Get the plural formula and update in database. 1298 if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->filename)) { 1299 list($nplurals, $plural) = $p; 1300 db_query("UPDATE {languages} SET plurals = %d, formula = '%s' WHERE language = '%s'", $nplurals, $plural, $lang); 1301 } 1302 else { 1303 db_query("UPDATE {languages} SET plurals = %d, formula = '%s' WHERE language = '%s'", 0, '', $lang); 1304 } 1305 } 1306 $headerdone = TRUE; 1307 } 1308 1309 else { 1310 // Some real string to import. 1311 $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']); 1312 1313 if (strpos($value['msgid'], "\0")) { 1314 // This string has plural versions. 1315 $english = explode("\0", $value['msgid'], 2); 1316 $entries = array_keys($value['msgstr']); 1317 for ($i = 3; $i <= count($entries); $i++) { 1318 $english[] = $english[1]; 1319 } 1320 $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries); 1321 $english = array_map('_locale_import_append_plural', $english, $entries); 1322 foreach ($translation as $key => $trans) { 1323 if ($key == 0) { 1324 $plid = 0; 1325 } 1326 $plid = _locale_import_one_string_db($report, $lang, $english[$key], $trans, $group, $comments, $mode, $plid, $key); 1327 } 1328 } 1329 1330 else { 1331 // A simple string to import. 1332 $english = $value['msgid']; 1333 $translation = $value['msgstr']; 1334 _locale_import_one_string_db($report, $lang, $english, $translation, $group, $comments, $mode); 1335 } 1336 } 1337 } // end of db-store operation 1338 } 1339 1340 /** 1341 * Import one string into the database. 1342 * 1343 * @param $report 1344 * Report array summarizing the number of changes done in the form: 1345 * array(inserts, updates, deletes). 1346 * @param $langcode 1347 * Language code to import string into. 1348 * @param $source 1349 * Source string. 1350 * @param $translation 1351 * Translation to language specified in $langcode. 1352 * @param $textgroup 1353 * Name of textgroup to store translation in. 1354 * @param $location 1355 * Location value to save with source string. 1356 * @param $mode 1357 * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE. 1358 * @param $plid 1359 * Optional plural ID to use. 1360 * @param $plural 1361 * Optional plural value to use. 1362 * @return 1363 * The string ID of the existing string modified or the new string added. 1364 */ 1365 function _locale_import_one_string_db(&$report, $langcode, $source, $translation, $textgroup, $location, $mode, $plid = NULL, $plural = NULL) { 1366 $lid = db_result(db_query("SELECT lid FROM {locales_source} WHERE source = '%s' AND textgroup = '%s'", $source, $textgroup)); 1367 1368 if (!empty($translation)) { 1369 // Skip this string unless it passes a check for dangerous code. 1370 // Text groups other than default still can contain HTML tags 1371 // (i.e. translatable blocks). 1372 if ($textgroup == "default" && !locale_string_is_safe($translation)) { 1373 $report['skips']++; 1374 $lid = 0; 1375 } 1376 elseif ($lid) { 1377 // We have this source string saved already. 1378 db_query("UPDATE {locales_source} SET location = '%s' WHERE lid = %d", $location, $lid); 1379 $exists = (bool) db_result(db_query("SELECT lid FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $langcode)); 1380 if (!$exists) { 1381 // No translation in this language. 1382 db_query("INSERT INTO {locales_target} (lid, language, translation, plid, plural) VALUES (%d, '%s', '%s', %d, %d)", $lid, $langcode, $translation, $plid, $plural); 1383 $report['additions']++; 1384 } 1385 else if ($mode == LOCALE_IMPORT_OVERWRITE) { 1386 // Translation exists, only overwrite if instructed. 1387 db_query("UPDATE {locales_target} SET translation = '%s', plid = %d, plural = %d WHERE language = '%s' AND lid = %d", $translation, $plid, $plural, $langcode, $lid); 1388 $report['updates']++; 1389 } 1390 } 1391 else { 1392 // No such source string in the database yet. 1393 db_query("INSERT INTO {locales_source} (location, source, textgroup) VALUES ('%s', '%s', '%s')", $location, $source, $textgroup); 1394 $lid = db_result(db_query("SELECT lid FROM {locales_source} WHERE source = '%s' AND textgroup = '%s'", $source, $textgroup)); 1395 db_query("INSERT INTO {locales_target} (lid, language, translation, plid, plural) VALUES (%d, '%s', '%s', %d, %d)", $lid, $langcode, $translation, $plid, $plural); 1396 $report['additions']++; 1397 } 1398 } 1399 elseif ($mode == LOCALE_IMPORT_OVERWRITE) { 1400 // Empty translation, remove existing if instructed. 1401 db_query("DELETE FROM {locales_target} WHERE language = '%s' AND lid = %d AND plid = %d AND plural = %d", $langcode, $lid, $plid, $plural); 1402 $report['deletes']++; 1403 } 1404 1405 return $lid; 1406 } 1407 1408 /** 1409 * Parses a Gettext Portable Object file header 1410 * 1411 * @param $header 1412 * A string containing the complete header 1413 * @return 1414 * An associative array of key-value pairs 1415 */ 1416 function _locale_import_parse_header($header) { 1417 $header_parsed = array(); 1418 $lines = array_map('trim', explode("\n", $header)); 1419 foreach ($lines as $line) { 1420 if ($line) { 1421 list($tag, $contents) = explode(":", $line, 2); 1422 $header_parsed[trim($tag)] = trim($contents); 1423 } 1424 } 1425 return $header_parsed; 1426 } 1427 1428 /** 1429 * Parses a Plural-Forms entry from a Gettext Portable Object file header 1430 * 1431 * @param $pluralforms 1432 * A string containing the Plural-Forms entry 1433 * @param $filename 1434 * A string containing the filename 1435 * @return 1436 * An array containing the number of plurals and a 1437 * formula in PHP for computing the plural form 1438 */ 1439 function _locale_import_parse_plural_forms($pluralforms, $filename) { 1440 // First, delete all whitespace 1441 $pluralforms = strtr($pluralforms, array(" " => "", "\t" => "")); 1442 1443 // Select the parts that define nplurals and plural 1444 $nplurals = strstr($pluralforms, "nplurals="); 1445 if (strpos($nplurals, ";")) { 1446 $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9); 1447 } 1448 else { 1449 return FALSE; 1450 } 1451 $plural = strstr($pluralforms, "plural="); 1452 if (strpos($plural, ";")) { 1453 $plural = substr($plural, 7, strpos($plural, ";") - 7); 1454 } 1455 else { 1456 return FALSE; 1457 } 1458 1459 // Get PHP version of the plural formula 1460 $plural = _locale_import_parse_arithmetic($plural); 1461 1462 if ($plural !== FALSE) { 1463 return array($nplurals, $plural); 1464 } 1465 else { 1466 drupal_set_message(t('The translation file %filename contains an error: the plural formula could not be parsed.', array('%filename' => $filename)), 'error'); 1467 return FALSE; 1468 } 1469 } 1470 1471 /** 1472 * Parses and sanitizes an arithmetic formula into a PHP expression 1473 * 1474 * While parsing, we ensure, that the operators have the right 1475 * precedence and associativity. 1476 * 1477 * @param $string 1478 * A string containing the arithmetic formula 1479 * @return 1480 * The PHP version of the formula 1481 */ 1482 function _locale_import_parse_arithmetic($string) { 1483 // Operator precedence table 1484 $prec = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8); 1485 // Right associativity 1486 $rasc = array("?" => 1, ":" => 1); 1487 1488 $tokens = _locale_import_tokenize_formula($string); 1489 1490 // Parse by converting into infix notation then back into postfix 1491 $opstk = array(); 1492 $elstk = array(); 1493 1494 foreach ($tokens as $token) { 1495 $ctok = $token; 1496 1497 // Numbers and the $n variable are simply pushed into $elarr 1498 if (is_numeric($token)) { 1499 $elstk[] = $ctok; 1500 } 1501 elseif ($ctok == "n") { 1502 $elstk[] = '$n'; 1503 } 1504 elseif ($ctok == "(") { 1505 $opstk[] = $ctok; 1506 } 1507 elseif ($ctok == ")") { 1508 $topop = array_pop($opstk); 1509 while (isset($topop) && ($topop != "(")) { 1510 $elstk[] = $topop; 1511 $topop = array_pop($opstk); 1512 } 1513 } 1514 elseif (!empty($prec[$ctok])) { 1515 // If it's an operator, then pop from $oparr into $elarr until the 1516 // precedence in $oparr is less than current, then push into $oparr 1517 $topop = array_pop($opstk); 1518 while (isset($topop) && ($prec[$topop] >= $prec[$ctok]) && !(($prec[$topop] == $prec[$ctok]) && !empty($rasc[$topop]) && !empty($rasc[$ctok]))) { 1519 $elstk[] = $topop; 1520 $topop = array_pop($opstk); 1521 } 1522 if ($topop) { 1523 $opstk[] = $topop; // Return element to top 1524 } 1525 $opstk[] = $ctok; // Parentheses are not needed 1526 } 1527 else { 1528 return FALSE; 1529 } 1530 } 1531 1532 // Flush operator stack 1533 $topop = array_pop($opstk); 1534 while ($topop != NULL) { 1535 $elstk[] = $topop; 1536 $topop = array_pop($opstk); 1537 } 1538 1539 // Now extract formula from stack 1540 $prevsize = count($elstk) + 1; 1541 while (count($elstk) < $prevsize) { 1542 $prevsize = count($elstk); 1543 for ($i = 2; $i < count($elstk); $i++) { 1544 $op = $elstk[$i]; 1545 if (!empty($prec[$op])) { 1546 $f = ""; 1547 if ($op == ":") { 1548 $f = $elstk[$i - 2] ."):". $elstk[$i - 1] .")"; 1549 } 1550 elseif ($op == "?") { 1551 $f = "(". $elstk[$i - 2] ."?(". $elstk[$i - 1]; 1552 } 1553 else { 1554 $f = "(". $elstk[$i - 2] . $op . $elstk[$i - 1] .")"; 1555 } 1556 array_splice($elstk, $i - 2, 3, $f); 1557 break; 1558 } 1559 } 1560 } 1561 1562 // If only one element is left, the number of operators is appropriate 1563 if (count($elstk) == 1) { 1564 return $elstk[0]; 1565 } 1566 else { 1567 return FALSE; 1568 } 1569 } 1570 1571 /** 1572 * Backward compatible implementation of token_get_all() for formula parsing 1573 * 1574 * @param $string 1575 * A string containing the arithmetic formula 1576 * @return 1577 * The PHP version of the formula 1578 */ 1579 function _locale_import_tokenize_formula($formula) { 1580 $formula = str_replace(" ", "", $formula); 1581 $tokens = array(); 1582 for ($i = 0; $i < strlen($formula); $i++) { 1583 if (is_numeric($formula[$i])) { 1584 $num = $formula[$i]; 1585 $j = $i + 1; 1586 while ($j < strlen($formula) && is_numeric($formula[$j])) { 1587 $num .= $formula[$j]; 1588 $j++; 1589 } 1590 $i = $j - 1; 1591 $tokens[] = $num; 1592 } 1593 elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space 1594 $next = $formula[$i + 1]; 1595 switch ($pos) { 1596 case 1: 1597 case 2: 1598 case 3: 1599 case 4: 1600 if ($next == '=') { 1601 $tokens[] = $formula[$i] .'='; 1602 $i++; 1603 } 1604 else { 1605 $tokens[] = $formula[$i]; 1606 } 1607 break; 1608 case 5: 1609 if ($next == '&') { 1610 $tokens[] = '&&'; 1611 $i++; 1612 } 1613 else { 1614 $tokens[] = $formula[$i]; 1615 } 1616 break; 1617 case 6: 1618 if ($next == '|') { 1619 $tokens[] = '||'; 1620 $i++; 1621 } 1622 else { 1623 $tokens[] = $formula[$i]; 1624 } 1625 break; 1626 } 1627 } 1628 else { 1629 $tokens[] = $formula[$i]; 1630 } 1631 } 1632 return $tokens; 1633 } 1634 1635 /** 1636 * Modify a string to contain proper count indices 1637 * 1638 * This is a callback function used via array_map() 1639 * 1640 * @param $entry 1641 * An array element 1642 * @param $key 1643 * Index of the array element 1644 */ 1645 function _locale_import_append_plural($entry, $key) { 1646 // No modifications for 0, 1 1647 if ($key == 0 || $key == 1) { 1648 return $entry; 1649 } 1650 1651 // First remove any possibly false indices, then add new ones 1652 $entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry); 1653 return preg_replace('/(@count)/', "\\1[$key]", $entry); 1654 } 1655 1656 /** 1657 * Generate a short, one string version of the passed comment array 1658 * 1659 * @param $comment 1660 * An array of strings containing a comment 1661 * @return 1662 * Short one string version of the comment 1663 */ 1664 function _locale_import_shorten_comments($comment) { 1665 $comm = ''; 1666 while (count($comment)) { 1667 $test = $comm . substr(array_shift($comment), 1) .', '; 1668 if (strlen($comm) < 130) { 1669 $comm = $test; 1670 } 1671 else { 1672 break; 1673 } 1674 } 1675 return substr($comm, 0, -2); 1676 } 1677 1678 /** 1679 * Parses a string in quotes 1680 * 1681 * @param $string 1682 * A string specified with enclosing quotes 1683 * @return 1684 * The string parsed from inside the quotes 1685 */ 1686 function _locale_import_parse_quoted($string) { 1687 if (substr($string, 0, 1) != substr($string, -1, 1)) { 1688 return FALSE; // Start and end quotes must be the same 1689 } 1690 $quote = substr($string, 0, 1); 1691 $string = substr($string, 1, -1); 1692 if ($quote == '"') { // Double quotes: strip slashes 1693 return stripcslashes($string); 1694 } 1695 elseif ($quote == "'") { // Simple quote: return as-is 1696 return $string; 1697 } 1698 else { 1699 return FALSE; // Unrecognized quote 1700 } 1701 } 1702 /** 1703 * @} End of "locale-api-import" 1704 */ 1705 1706 /** 1707 * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and 1708 * Drupal.formatPlural() and inserts them into the database. 1709 */ 1710 function _locale_parse_js_file($filepath) { 1711 global $language; 1712 1713 // Load the JavaScript file. 1714 $file = file_get_contents($filepath); 1715 1716 // Match all calls to Drupal.t() in an array. 1717 // Note: \s also matches newlines with the 's' modifier. 1718 preg_match_all('~[^\w]Drupal\s*\.\s*t\s*\(\s*('. LOCALE_JS_STRING .')\s*[,\)]~s', $file, $t_matches); 1719 1720 // Match all Drupal.formatPlural() calls in another array. 1721 preg_match_all('~[^\w]Drupal\s*\.\s*formatPlural\s*\(\s*.+?\s*,\s*('. LOCALE_JS_STRING .')\s*,\s*((?:(?:\'(?:\\\\\'|[^\'])*@count(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*@count(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s*[,\)]~s', $file, $plural_matches); 1722 1723 // Loop through all matches and process them. 1724 $all_matches = array_merge($plural_matches[1], $t_matches[1]); 1725 foreach ($all_matches as $key => $string) { 1726 $strings = array($string); 1727 1728 // If there is also a plural version of this string, add it to the strings array. 1729 if (isset($plural_matches[2][$key])) { 1730 $strings[] = $plural_matches[2][$key]; 1731 } 1732 1733 foreach ($strings as $key => $string) { 1734 // Remove the quotes and string concatenations from the string. 1735 $string = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($string, 1, -1))); 1736 1737 $result = db_query("SELECT lid, location FROM {locales_source} WHERE source = '%s' AND textgroup = 'default'", $string); 1738 if ($source = db_fetch_object($result)) { 1739 // We already have this source string and now have to add the location 1740 // to the location column, if this file is not yet present in there. 1741 $locations = preg_split('~\s*;\s*~', $source->location); 1742 1743 if (!in_array($filepath, $locations)) { 1744 $locations[] = $filepath; 1745 $locations = implode('; ', $locations); 1746 1747 // Save the new locations string to the database. 1748 db_query("UPDATE {locales_source} SET location = '%s' WHERE lid = %d", $locations, $source->lid); 1749 } 1750 } 1751 else { 1752 // We don't have the source string yet, thus we insert it into the database. 1753 db_query("INSERT INTO {locales_source} (location, source, textgroup) VALUES ('%s', '%s', 'default')", $filepath, $string); 1754 } 1755 } 1756 } 1757 } 1758 1759 /** 1760 * @defgroup locale-api-export Translation (template) export API. 1761 * @{ 1762 */ 1763 1764 /** 1765 * Generates a structured array of all strings with translations in 1766 * $language, if given. This array can be used to generate an export 1767 * of the string in the database. 1768 * 1769 * @param $language 1770 * Language object to generate the output for, or NULL if generating 1771 * translation template. 1772 * @param $group 1773 * Text group to export PO file from (eg. 'default' for interface translations) 1774 */ 1775 function _locale_export_get_strings($language = NULL, $group = 'default') { 1776 if (isset($language)) { 1777 $result = db_query("SELECT s.lid, s.source, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.textgroup = '%s' ORDER BY t.plid, t.plural", $language->language, $group); 1778 } 1779 else { 1780 $result = db_query("SELECT s.lid, s.source, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.textgroup = '%s' ORDER BY t.plid, t.plural", $group); 1781 } 1782 $strings = array(); 1783 while ($child = db_fetch_object($result)) { 1784 $string = array( 1785 'comment' => $child->location, 1786 'source' => $child->source, 1787 'translation' => isset($child->translation) ? $child->translation : '' 1788 ); 1789 if ($child->plid) { 1790 // Has a parent lid. Since we process in the order of plids, 1791 // we already have the parent in the array, so we can add the 1792 // lid to the next plural version to it. This builds a linked 1793 // list of plurals. 1794 $string['child'] = TRUE; 1795 $strings[$child->plid]['plural'] = $child->lid; 1796 } 1797 $strings[$child->lid] = $string; 1798 } 1799 return $strings; 1800 } 1801 1802 /** 1803 * Generates the PO(T) file contents for given strings. 1804 * 1805 * @param $language 1806 * Language object to generate the output for, or NULL if generating 1807 * translation template. 1808 * @param $strings 1809 * Array of strings to export. See _locale_export_get_strings() 1810 * on how it should be formatted. 1811 * @param $header 1812 * The header portion to use for the output file. Defaults 1813 * are provided for PO and POT files. 1814 */ 1815 function _locale_export_po_generate($language = NULL, $strings = array(), $header = NULL) { 1816 global $user; 1817 1818 if (!isset($header)) { 1819 if (isset($language)) { 1820 $header = '# '. $language->name .' translation of '. variable_get('site_name', 'Drupal') ."\n"; 1821 $header .= '# Generated by '. $user->name .' <'. $user->mail .">\n"; 1822 $header .= "#\n"; 1823 $header .= "msgid \"\"\n"; 1824 $header .= "msgstr \"\"\n"; 1825 $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; 1826 $header .= "\"POT-Creation-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; 1827 $header .= "\"PO-Revision-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; 1828 $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n"; 1829 $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n"; 1830 $header .= "\"MIME-Version: 1.0\\n\"\n"; 1831 $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; 1832 $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; 1833 if ($language->formula && $language->plurals) { 1834 $header .= "\"Plural-Forms: nplurals=". $language->plurals ."; plural=". strtr($language->formula, array('$' => '')) .";\\n\"\n"; 1835 } 1836 } 1837 else { 1838 $header = "# LANGUAGE translation of PROJECT\n"; 1839 $header .= "# Copyright (c) YEAR NAME <EMAIL@ADDRESS>\n"; 1840 $header .= "#\n"; 1841 $header .= "msgid \"\"\n"; 1842 $header .= "msgstr \"\"\n"; 1843 $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; 1844 $header .= "\"POT-Creation-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; 1845 $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; 1846 $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n"; 1847 $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n"; 1848 $header .= "\"MIME-Version: 1.0\\n\"\n"; 1849 $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; 1850 $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; 1851 $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"; 1852 } 1853 } 1854 1855 $output = $header ."\n"; 1856 1857 foreach ($strings as $lid => $string) { 1858 // Only process non-children, children are output below their parent. 1859 if (!isset($string['child'])) { 1860 if ($string['comment']) { 1861 $output .= '#: '. $string['comment'] ."\n"; 1862 } 1863 $output .= 'msgid '. _locale_export_string($string['source']); 1864 if (!empty($string['plural'])) { 1865 $plural = $string['plural']; 1866 $output .= 'msgid_plural '. _locale_export_string($strings[$plural]['source']); 1867 if (isset($language)) { 1868 $translation = $string['translation']; 1869 for ($i = 0; $i < $language->plurals; $i++) { 1870 $output .= 'msgstr['. $i .'] '. _locale_export_string($translation); 1871 if ($plural) { 1872 $translation = _locale_export_remove_plural($strings[$plural]['translation']); 1873 $plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0; 1874 } 1875 else { 1876 $translation = ''; 1877 } 1878 } 1879 } 1880 else { 1881 $output .= 'msgstr[0] ""'."\n"; 1882 $output .= 'msgstr[1] ""'."\n"; 1883 } 1884 } 1885 else { 1886 $output .= 'msgstr '. _locale_export_string($string['translation']); 1887 } 1888 $output .= "\n"; 1889 } 1890 } 1891 return $output; 1892 } 1893 1894 /** 1895 * Write a generated PO or POT file to the output. 1896 * 1897 * @param $language 1898 * Language object to generate the output for, or NULL if generating 1899 * translation template. 1900 * @param $output 1901 * The PO(T) file to output as a string. See _locale_export_generate_po() 1902 * on how it can be generated. 1903 */ 1904 function _locale_export_po($language = NULL, $output = NULL) { 1905 // Log the export event. 1906 if (isset($language)) { 1907 $filename = $language->language .'.po'; 1908 watchdog('locale', 'Exported %locale translation file: %filename.', array('%locale' => $language->name, '%filename' => $filename)); 1909 } 1910 else { 1911 $filename = 'drupal.pot'; 1912 watchdog('locale', 'Exported translation file: %filename.', array('%filename' => $filename)); 1913 } 1914 // Download the file fo the client. 1915 header("Content-Disposition: attachment; filename=$filename"); 1916 header("Content-Type: text/plain; charset=utf-8"); 1917 print $output; 1918 die(); 1919 } 1920 1921 /** 1922 * Print out a string on multiple lines 1923 */ 1924 function _locale_export_string($str) { 1925 $stri = addcslashes($str, "\0..\37\\\""); 1926 $parts = array(); 1927 1928 // Cut text into several lines 1929 while ($stri != "") { 1930 $i = strpos($stri, "\\n"); 1931 if ($i === FALSE) { 1932 $curstr = $stri; 1933 $stri = ""; 1934 } 1935 else { 1936 $curstr = substr($stri, 0, $i + 2); 1937 $stri = substr($stri, $i + 2); 1938 } 1939 $curparts = explode("\n", _locale_export_wrap($curstr, 70)); 1940 $parts = array_merge($parts, $curparts); 1941 } 1942 1943 // Multiline string 1944 if (count($parts) > 1) { 1945 return "\"\"\n\"". implode("\"\n\"", $parts) ."\"\n"; 1946 } 1947 // Single line string 1948 elseif (count($parts) == 1) { 1949 return "\"$parts[0]\"\n"; 1950 } 1951 // No translation 1952 else { 1953 return "\"\"\n"; 1954 } 1955 } 1956 1957 /** 1958 * Custom word wrapping for Portable Object (Template) files. 1959 */ 1960 function _locale_export_wrap($str, $len) { 1961 $words = explode(' ', $str); 1962 $ret = array(); 1963 1964 $cur = ""; 1965 $nstr = 1; 1966 while (count($words)) { 1967 $word = array_shift($words); 1968 if ($nstr) { 1969 $cur = $word; 1970 $nstr = 0; 1971 } 1972 elseif (strlen("$cur $word") > $len) { 1973 $ret[] = $cur ." "; 1974 $cur = $word; 1975 } 1976 else { 1977 $cur = "$cur $word"; 1978 } 1979 } 1980 $ret[] = $cur; 1981 1982 return implode("\n", $ret); 1983 } 1984 1985 /** 1986 * Removes plural index information from a string 1987 */ 1988 function _locale_export_remove_plural($entry) { 1989 return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry); 1990 } 1991 /** 1992 * @} End of "locale-api-export" 1993 */ 1994 1995 /** 1996 * @defgroup locale-api-seek String search functions. 1997 * @{ 1998 */ 1999 2000 /** 2001 * Perform a string search and display results in a table 2002 */ 2003 function _locale_translate_seek() { 2004 $output = ''; 2005 2006 // We have at least one criterion to match 2007 if ($query = _locale_translate_seek_query()) { 2008 $join = "SELECT s.source, s.location, s.lid, s.textgroup, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid "; 2009 2010 $arguments = array(); 2011 $limit_language = FALSE; 2012 // Compute LIKE section 2013 switch ($query['translation']) { 2014 case 'translated': 2015 $where = "WHERE (t.translation LIKE '%%%s%%')"; 2016 $orderby = "ORDER BY t.translation"; 2017 $arguments[] = $query['string']; 2018 break; 2019 case 'untranslated': 2020 $where = "WHERE (s.source LIKE '%%%s%%' AND t.translation IS NULL)"; 2021 $orderby = "ORDER BY s.source"; 2022 $arguments[] = $query['string']; 2023 break; 2024 case 'all' : 2025 default: 2026 $where = "WHERE (s.source LIKE '%%%s%%' OR t.translation LIKE '%%%s%%')"; 2027 $orderby = ''; 2028 $arguments[] = $query['string']; 2029 $arguments[] = $query['string']; 2030 break; 2031 } 2032 $grouplimit = ''; 2033 if (!empty($query['group']) && $query['group'] != 'all') { 2034 $grouplimit = " AND s.textgroup = '%s'"; 2035 $arguments[] = $query['group']; 2036 } 2037 2038 switch ($query['language']) { 2039 // Force search in source strings 2040 case "en": 2041 $sql = $join ." WHERE s.source LIKE '%%%s%%' $grouplimit ORDER BY s.source"; 2042 $arguments = array($query['string']); // $where is not used, discard its arguments 2043 if (!empty($grouplimit)) { 2044 $arguments[] = $query['group']; 2045 } 2046 break; 2047 // Search in all languages 2048 case "all": 2049 $sql = "$join $where $grouplimit $orderby"; 2050 break; 2051 // Some different language 2052 default: 2053 $sql = "$join AND t.language = '%s' $where $grouplimit $orderby"; 2054 array_unshift($arguments, $query['language']); 2055 // Don't show translation flags for other languages, we can't see them with this search. 2056 $limit_language = $query['language']; 2057 } 2058 2059 $result = pager_query($sql, 50, 0, NULL, $arguments); 2060 2061 $groups = module_invoke_all('locale', 'groups'); 2062 $header = array(t('Text group'), t('String'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2')); 2063 $arr = array(); 2064 while ($locale = db_fetch_object($result)) { 2065 $arr[$locale->lid]['group'] = $groups[$locale->textgroup]; 2066 $arr[$locale->lid]['languages'][$locale->language] = $locale->translation; 2067 $arr[$locale->lid]['location'] = $locale->location; 2068 $arr[$locale->lid]['source'] = $locale->source; 2069 } 2070 $rows = array(); 2071 foreach ($arr as $lid => $value) { 2072 $rows[] = array( 2073 $value['group'], 2074 array('data' => check_plain(truncate_utf8($value['source'], 150, FALSE, TRUE)) .'<br /><small>'. $value['location'] .'</small>'), 2075 array('data' => _locale_translate_language_list($value['languages'], $limit_language), 'align' => 'center'), 2076 array('data' => l(t('edit'), "admin/build/translate/edit/$lid"), 'class' => 'nowrap'), 2077 array('data' => l(t('delete'), "admin/build/translate/delete/$lid"), 'class' => 'nowrap'), 2078 ); 2079 } 2080 2081 if (count($rows)) { 2082 $output .= theme('table', $header, $rows); 2083 if ($pager = theme('pager', NULL, 50)) { 2084 $output .= $pager; 2085 } 2086 } 2087 else { 2088 $output .= t('No strings found for your search.'); 2089 } 2090 } 2091 2092 return $output; 2093 } 2094 2095 /** 2096 * Build array out of search criteria specified in request variables 2097 */ 2098 function _locale_translate_seek_query() { 2099 static $query = NULL; 2100 if (!isset($query)) { 2101 $query = array(); 2102 $fields = array('string', 'language', 'translation', 'group'); 2103 foreach ($fields as $field) { 2104 if (isset($_REQUEST[$field])) { 2105 $query[$field] = $_REQUEST[$field]; 2106 } 2107 } 2108 } 2109 return $query; 2110 } 2111 2112 /** 2113 * Force the JavaScript translation file(s) to be refreshed. 2114 * 2115 * This function sets a refresh flag for a specified language, or all 2116 * languages except English, if none specified. JavaScript translation 2117 * files are rebuilt (with locale_update_js_files()) the next time a 2118 * request is served in that language. 2119 * 2120 * @param $langcode 2121 * The language code for which the file needs to be refreshed. 2122 * @return 2123 * New content of the 'javascript_parsed' variable. 2124 */ 2125 function _locale_invalidate_js($langcode = NULL) { 2126 $parsed = variable_get('javascript_parsed', array()); 2127 2128 if (empty($langcode)) { 2129 // Invalidate all languages. 2130 $languages = language_list(); 2131 unset($languages['en']); 2132 foreach ($languages as $lcode => $data) { 2133 $parsed['refresh:'. $lcode] = 'waiting'; 2134 } 2135 } 2136 else { 2137 // Invalidate single language. 2138 $parsed['refresh:'. $langcode] = 'waiting'; 2139 } 2140 2141 variable_set('javascript_parsed', $parsed); 2142 return $parsed; 2143 } 2144 2145 /** 2146 * (Re-)Creates the JavaScript translation file for a language. 2147 * 2148 * @param $language 2149 * The language, the translation file should be (re)created for. 2150 */ 2151 function _locale_rebuild_js($langcode = NULL) { 2152 if (!isset($langcode)) { 2153 global $language; 2154 } 2155 else { 2156 // Get information about the locale. 2157 $languages = language_list(); 2158 $language = $languages[$langcode]; 2159 } 2160 2161 // Construct the array for JavaScript translations. 2162 // We sort on plural so that we have all plural forms before singular forms. 2163 $result = db_query("SELECT s.lid, s.source, t.plid, t.plural, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.location LIKE '%%.js%%' AND s.textgroup = 'default' ORDER BY t.plural DESC", $language->language); 2164 2165 $translations = $plurals = array(); 2166 while ($data = db_fetch_object($result)) { 2167 // Only add this to the translations array when there is actually a translation. 2168 if (!empty($data->translation)) { 2169 if ($data->plural) { 2170 // When the translation is a plural form, first add it to another array and 2171 // wait for the singular (parent) translation. 2172 if (!isset($plurals[$data->plid])) { 2173 $plurals[$data->plid] = array($data->plural => $data->translation); 2174 } 2175 else { 2176 $plurals[$data->plid] += array($data->plural => $data->translation); 2177 } 2178 } 2179 elseif (isset($plurals[$data->lid])) { 2180 // There are plural translations for this translation, so get them from 2181 // the plurals array and add them to the final translations array. 2182 $translations[$data->source] = array($data->plural => $data->translation) + $plurals[$data->lid]; 2183 unset($plurals[$data->lid]); 2184 } 2185 else { 2186 // There are no plural forms for this translation, so just add it to 2187 // the translations array. 2188 $translations[$data->source] = $data->translation; 2189 } 2190 } 2191 } 2192 2193 // Construct the JavaScript file, if there are translations. 2194 $data_hash = NULL; 2195 $data = $status = ''; 2196 if (!empty($translations)) { 2197 2198 $data = "Drupal.locale = { "; 2199 2200 if (!empty($language->formula)) { 2201 $data .= "'pluralFormula': function(\$n) { return Number({$language->formula}); }, "; 2202 } 2203 2204 $data .= "'strings': ". drupal_to_js($translations) ." };"; 2205 $data_hash = md5($data); 2206 } 2207 2208 // Construct the filepath where JS translation files are stored. 2209 // There is (on purpose) no front end to edit that variable. 2210 $dir = file_create_path(variable_get('locale_js_directory', 'languages')); 2211 2212 // Delete old file, if we have no translations anymore, or a different file to be saved. 2213 $changed_hash = $language->javascript != $data_hash; 2214 if (!empty($language->javascript) && (!$data || $changed_hash)) { 2215 file_delete(file_create_path($dir .'/'. $language->language .'_'. $language->javascript .'.js')); 2216 $language->javascript = ''; 2217 $status = 'deleted'; 2218 } 2219 2220 // Only create a new file if the content has changed or the original file got 2221 // lost. 2222 $dest = $dir .'/'. $language->language .'_'. $data_hash .'.js'; 2223 if ($data && ($changed_hash || !file_exists($dest))) { 2224 // Ensure that the directory exists and is writable, if possible. 2225 file_check_directory($dir, TRUE); 2226 2227 // Save the file. 2228 if (file_save_data($data, $dest)) { 2229 $language->javascript = $data_hash; 2230 // If we deleted a previous version of the file and we replace it with a 2231 // new one we have an update. 2232 if ($status == 'deleted') { 2233 $status = 'updated'; 2234 } 2235 // If the file did not exist previously and the data has changed we have 2236 // a fresh creation. 2237 elseif ($changed_hash) { 2238 $status = 'created'; 2239 } 2240 // If the data hash is unchanged the translation was lost and has to be 2241 // rebuilt. 2242 else { 2243 $status = 'rebuilt'; 2244 } 2245 } 2246 else { 2247 $language->javascript = ''; 2248 $status = 'error'; 2249 } 2250 } 2251 2252 // Save the new JavaScript hash (or an empty value if the file just got 2253 // deleted). Act only if some operation was executed that changed the hash 2254 // code. 2255 if ($status && $changed_hash) { 2256 db_query("UPDATE {languages} SET javascript = '%s' WHERE language = '%s'", $language->javascript, $language->language); 2257 2258 // Update the default language variable if the default language has been altered. 2259 // This is necessary to keep the variable consistent with the database 2260 // version of the language and to prevent checking against an outdated hash. 2261 $default_langcode = language_default('language'); 2262 if ($default_langcode == $language->language) { 2263 $default = db_fetch_object(db_query("SELECT * FROM {languages} WHERE language = '%s'", $default_langcode)); 2264 variable_set('language_default', $default); 2265 } 2266 } 2267 2268 // Log the operation and return success flag. 2269 switch ($status) { 2270 case 'updated': 2271 watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => t($language->name))); 2272 return TRUE; 2273 case 'rebuilt': 2274 watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $language->javascript), WATCHDOG_WARNING); 2275 // Proceed to the 'created' case as the JavaScript translation file has 2276 // been created again. 2277 case 'created': 2278 watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => t($language->name))); 2279 return TRUE; 2280 case 'deleted': 2281 watchdog('locale', 'Removed JavaScript translation file for the language %language, because no translations currently exist for that language.', array('%language' => t($language->name))); 2282 return TRUE; 2283 case 'error': 2284 watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => t($language->name)), WATCHDOG_ERROR); 2285 return FALSE; 2286 default: 2287 // No operation needed. 2288 return TRUE; 2289 } 2290 } 2291 2292 /** 2293 * List languages in search result table 2294 */ 2295 function _locale_translate_language_list($translation, $limit_language) { 2296 // Add CSS 2297 drupal_add_css(drupal_get_path('module', 'locale') .'/locale.css', 'module', 'all', FALSE); 2298 2299 $languages = language_list(); 2300 unset($languages['en']); 2301 $output = ''; 2302 foreach ($languages as $langcode => $language) { 2303 if (!$limit_language || $limit_language == $langcode) { 2304 $output .= (!empty($translation[$langcode])) ? $langcode .' ' : "<em class=\"locale-untranslated\">$langcode</em> "; 2305 } 2306 } 2307 2308 return $output; 2309 } 2310 /** 2311 * @} End of "locale-api-seek" 2312 */ 2313 2314 /** 2315 * @defgroup locale-api-predefined List of predefined languages 2316 * @{ 2317 */ 2318 2319 /** 2320 * Prepares the language code list for a select form item with only the unsupported ones 2321 */ 2322 function _locale_prepare_predefined_list() { 2323 $languages = language_list(); 2324 $predefined = _locale_get_predefined_list(); 2325 foreach ($predefined as $key => $value) { 2326 if (isset($languages[$key])) { 2327 unset($predefined[$key]); 2328 continue; 2329 } 2330 // Include native name in output, if possible 2331 if (count($value) > 1) { 2332 $tname = t($value[0]); 2333 $predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])"; 2334 } 2335 else { 2336 $predefined[$key] = t($value[0]); 2337 } 2338 } 2339 asort($predefined); 2340 return $predefined; 2341 } 2342 2343 /** 2344 * Some of the common languages with their English and native names 2345 * 2346 * Based on ISO 639 and http://people.w3.org/rishida/names/languages.html 2347 */ 2348 function _locale_get_predefined_list() { 2349 return array( 2350 "aa" => array("Afar"), 2351 "ab" => array("Abkhazian", "аҧсуа бызшәа"), 2352 "ae" => array("Avestan"), 2353 "af" => array("Afrikaans"), 2354 "ak" => array("Akan"), 2355 "am" => array("Amharic", "አማርኛ"), 2356 "ar" => array("Arabic", /* Left-to-right marker "" */ "العربية", LANGUAGE_RTL), 2357 "as" => array("Assamese"), 2358 "av" => array("Avar"), 2359 "ay" => array("Aymara"), 2360 "az" => array("Azerbaijani", "azərbaycan"), 2361 "ba" => array("Bashkir"), 2362 "be" => array("Belarusian", "Беларуская"), 2363 "bg" => array("Bulgarian", "Български"), 2364 "bh" => array("Bihari"), 2365 "bi" => array("Bislama"), 2366 "bm" => array("Bambara", "Bamanankan"), 2367 "bn" => array("Bengali"), 2368 "bo" => array("Tibetan"), 2369 "br" => array("Breton"), 2370 "bs" => array("Bosnian", "Bosanski"), 2371 "ca" => array("Catalan", "Català"), 2372 "ce" => array("Chechen"), 2373 "ch" => array("Chamorro"), 2374 "co" => array("Corsican"), 2375 "cr" => array("Cree"), 2376 "cs" => array("Czech", "Čeština"), 2377 "cu" => array("Old Slavonic"), 2378 "cv" => array("Chuvash"), 2379 "cy" => array("Welsh", "Cymraeg"), 2380 "da" => array("Danish", "Dansk"), 2381 "de" => array("German", "Deutsch"), 2382 "dv" => array("Maldivian"), 2383 "dz" => array("Bhutani"), 2384 "ee" => array("Ewe", "Ɛʋɛ"), 2385 "el" => array("Greek", "Ελληνικά"), 2386 "en" => array("English"), 2387 "eo" => array("Esperanto"), 2388 "es" => array("Spanish", "Español"), 2389 "et" => array("Estonian", "Eesti"), 2390 "eu" => array("Basque", "Euskera"), 2391 "fa" => array("Persian", /* Left-to-right marker "" */ "فارسی", LANGUAGE_RTL), 2392 "ff" => array("Fulah", "Fulfulde"), 2393 "fi" => array("Finnish", "Suomi"), 2394 "fj" => array("Fiji"), 2395 "fo" => array("Faeroese"), 2396 "fr" => array("French", "Français"), 2397 "fy" => array("Frisian", "Frysk"), 2398 "ga" => array("Irish", "Gaeilge"), 2399 "gd" => array("Scots Gaelic"), 2400 "gl" => array("Galician", "Galego"), 2401 "gn" => array("Guarani"), 2402 "gu" => array("Gujarati"), 2403 "gv" => array("Manx"), 2404 "ha" => array("Hausa"), 2405 "he" => array("Hebrew", /* Left-to-right marker "" */ "עברית", LANGUAGE_RTL), 2406 "hi" => array("Hindi", "हिन्दी"), 2407 "ho" => array("Hiri Motu"), 2408 "hr" => array("Croatian", "Hrvatski"), 2409 "hu" => array("Hungarian", "Magyar"), 2410 "hy" => array("Armenian", "Հայերեն"), 2411 "hz" => array("Herero"), 2412 "ia" => array("Interlingua"), 2413 "id" => array("Indonesian", "Bahasa Indonesia"), 2414 "ie" => array("Interlingue"), 2415 "ig" => array("Igbo"), 2416 "ik" => array("Inupiak"), 2417 "is" => array("Icelandic", "Íslenska"), 2418 "it" => array("Italian", "Italiano"), 2419 "iu" => array("Inuktitut"), 2420 "ja" => array("Japanese", "日本語"), 2421 "jv" => array("Javanese"), 2422 "ka" => array("Georgian"), 2423 "kg" => array("Kongo"), 2424 "ki" => array("Kikuyu"), 2425 "kj" => array("Kwanyama"), 2426 "kk" => array("Kazakh", "Қазақ"), 2427 "kl" => array("Greenlandic"), 2428 "km" => array("Cambodian"), 2429 "kn" => array("Kannada", "ಕನ್ನಡ"), 2430 "ko" => array("Korean", "한국어"), 2431 "kr" => array("Kanuri"), 2432 "ks" => array("Kashmiri"), 2433 "ku" => array("Kurdish", "Kurdî"), 2434 "kv" => array("Komi"), 2435 "kw" => array("Cornish"), 2436 "ky" => array("Kirghiz", "Кыргыз"), 2437 "la" => array("Latin", "Latina"), 2438 "lb" => array("Luxembourgish"), 2439 "lg" => array("Luganda"), 2440 "ln" => array("Lingala"), 2441 "lo" => array("Laothian"), 2442 "lt" => array("Lithuanian", "Lietuvių"), 2443 "lv" => array("Latvian", "Latviešu"), 2444 "mg" => array("Malagasy"), 2445 "mh" => array("Marshallese"), 2446 "mi" => array("Maori"), 2447 "mk" => array("Macedonian", "Македонски"), 2448 "ml" => array("Malayalam", "മലയാളം"), 2449 "mn" => array("Mongolian"), 2450 "mo" => array("Moldavian"), 2451 "mr" => array("Marathi"), 2452 "ms" => array("Malay", "Bahasa Melayu"), 2453 "mt" => array("Maltese", "Malti"), 2454 "my" => array("Burmese"), 2455 "na" => array("Nauru"), 2456 "nd" => array("North Ndebele"), 2457 "ne" => array("Nepali"), 2458 "ng" => array("Ndonga"), 2459 "nl" => array("Dutch", "Nederlands"), 2460 "nb" => array("Norwegian Bokmål", "Bokmål"), 2461 "nn" => array("Norwegian Nynorsk", "Nynorsk"), 2462 "nr" => array("South Ndebele"), 2463 "nv" => array("Navajo"), 2464 "ny" => array("Chichewa"), 2465 "oc" => array("Occitan"), 2466 "om" => array("Oromo"), 2467 "or" => array("Oriya"), 2468 "os" => array("Ossetian"), 2469 "pa" => array("Punjabi"), 2470 "pi" => array("Pali"), 2471 "pl" => array("Polish", "Polski"), 2472 "ps" => array("Pashto", /* Left-to-right marker "" */ "پښتو", LANGUAGE_RTL), 2473 "pt-pt" => array("Portuguese, Portugal", "Português"), 2474 "pt-br" => array("Portuguese, Brazil", "Português"), 2475 "qu" => array("Quechua"), 2476 "rm" => array("Rhaeto-Romance"), 2477 "rn" => array("Kirundi"), 2478 "ro" => array("Romanian", "Română"), 2479 "ru" => array("Russian", "Русский"), 2480 "rw" => array("Kinyarwanda"), 2481 "sa" => array("Sanskrit"), 2482 "sc" => array("Sardinian"), 2483 "sd" => array("Sindhi"), 2484 "se" => array("Northern Sami"), 2485 "sg" => array("Sango"), 2486 "sh" => array("Serbo-Croatian"), 2487 "si" => array("Sinhala", "සිංහල"), 2488 "sk" => array("Slovak", "Slovenčina"), 2489 "sl" => array("Slovenian", "Slovenščina"), 2490 "sm" => array("Samoan"), 2491 "sn" => array("Shona"), 2492 "so" => array("Somali"), 2493 "sq" => array("Albanian", "Shqip"), 2494 "sr" => array("Serbian", "Српски"), 2495 "ss" => array("Siswati"), 2496 "st" => array("Sesotho"), 2497 "su" => array("Sudanese"), 2498 "sv" => array("Swedish", "Svenska"), 2499 "sw" => array("Swahili", "Kiswahili"), 2500 "ta" => array("Tamil", "தமிழ்"), 2501 "te" => array("Telugu", "తెలుగు"), 2502 "tg" => array("Tajik"), 2503 "th" => array("Thai", "ภาษาไทย"), 2504 "ti" => array("Tigrinya"), 2505 "tk" => array("Turkmen"), 2506 "tl" => array("Tagalog"), 2507 "tn" => array("Setswana"), 2508 "to" => array("Tonga"), 2509 "tr" => array("Turkish", "Türkçe"), 2510 "ts" => array("Tsonga"), 2511 "tt" => array("Tatar", "Tatarça"), 2512 "tw" => array("Twi"), 2513 "ty" => array("Tahitian"), 2514 "ug" => array("Uighur"), 2515 "uk" => array("Ukrainian", "Українська"), 2516 "ur" => array("Urdu", /* Left-to-right marker "" */ "اردو", LANGUAGE_RTL), 2517 "uz" => array("Uzbek", "o'zbek"), 2518 "ve" => array("Venda"), 2519 "vi" => array("Vietnamese", "Tiếng Việt"), 2520 "wo" => array("Wolof"), 2521 "xh" => array("Xhosa", "isiXhosa"), 2522 "yi" => array("Yiddish"), 2523 "yo" => array("Yoruba", "Yorùbá"), 2524 "za" => array("Zhuang"), 2525 "zh-hans" => array("Chinese, Simplified", "简体中文"), 2526 "zh-hant" => array("Chinese, Traditional", "繁體中文"), 2527 "zu" => array("Zulu", "isiZulu"), 2528 ); 2529 } 2530 /** 2531 * @} End of "locale-api-languages-predefined" 2532 */ 2533 2534 /** 2535 * @defgroup locale-autoimport Automatic interface translation import 2536 * @{ 2537 */ 2538 2539 /** 2540 * Prepare a batch to import translations for all enabled 2541 * modules in a given language. 2542 * 2543 * @param $langcode 2544 * Language code to import translations for. 2545 * @param $finished 2546 * Optional finished callback for the batch. 2547 * @param $skip 2548 * Array of component names to skip. Used in the installer for the 2549 * second pass import, when most components are already imported. 2550 * @return 2551 * A batch structure or FALSE if no files found. 2552 */ 2553 function locale_batch_by_language($langcode, $finished = NULL, $skip = array()) { 2554 // Collect all files to import for all enabled modules and themes. 2555 $files = array(); 2556 $components = array(); 2557 $query = "SELECT name, filename FROM {system} WHERE status = 1"; 2558 if (count($skip)) { 2559 $query .= " AND name NOT IN (". db_placeholders($skip, 'varchar') .")"; 2560 } 2561 $result = db_query($query, $skip); 2562 while ($component = db_fetch_object($result)) { 2563 // Collect all files for all components, names as $langcode.po or 2564 // with names ending with $langcode.po. This allows for filenames 2565 // like node-module.de.po to let translators use small files and 2566 // be able to import in smaller chunks. 2567 $files = array_merge($files, file_scan_directory(dirname($component->filename) .'/translations', '(^|\.)'. $langcode .'\.po$', array('.', '..', 'CVS'), 0, FALSE)); 2568 $components[] = $component->name; 2569 } 2570 2571 return _locale_batch_build($files, $finished, $components); 2572 } 2573 2574 /** 2575 * Prepare a batch to run when installing modules or enabling themes. 2576 * This batch will import translations for the newly added components 2577 * in all the languages already set up on the site. 2578 * 2579 * @param $components 2580 * An array of component (theme and/or module) names to import 2581 * translations for. 2582 * @param $finished 2583 * Optional finished callback for the batch. 2584 */ 2585 function locale_batch_by_component($components, $finished = '_locale_batch_system_finished') { 2586 $files = array(); 2587 $languages = language_list('enabled'); 2588 unset($languages[1]['en']); 2589 if (count($languages[1])) { 2590 $language_list = join('|', array_keys($languages[1])); 2591 // Collect all files to import for all $components. 2592 $result = db_query("SELECT name, filename FROM {system} WHERE status = 1"); 2593 while ($component = db_fetch_object($result)) { 2594 if (in_array($component->name, $components)) { 2595 // Collect all files for this component in all enabled languages, named 2596 // as $langcode.po or with names ending with $langcode.po. This allows 2597 // for filenames like node-module.de.po to let translators use small 2598 // files and be able to import in smaller chunks. 2599 $files = array_merge($files, file_scan_directory(dirname($component->filename) .'/translations', '(^|\.)('. $language_list .')\.po$', array('.', '..', 'CVS'), 0, FALSE)); 2600 } 2601 } 2602 return _locale_batch_build($files, $finished); 2603 } 2604 return FALSE; 2605 } 2606 2607 /** 2608 * Build a locale batch from an array of files. 2609 * 2610 * @param $files 2611 * Array of files to import 2612 * @param $finished 2613 * Optional finished callback for the batch. 2614 * @param $components 2615 * Optional list of component names the batch covers. Used in the installer. 2616 * @return 2617 * A batch structure 2618 */ 2619 function _locale_batch_build($files, $finished = NULL, $components = array()) { 2620 $t = get_t(); 2621 if (count($files)) { 2622 $operations = array(); 2623 foreach ($files as $file) { 2624 // We call _locale_batch_import for every batch operation. 2625 $operations[] = array('_locale_batch_import', array($file->filename)); } 2626 $batch = array( 2627 'operations' => $operations, 2628 'title' => $t('Importing interface translations'), 2629 'init_message' => $t('Starting import'), 2630 'error_message' => $t('Error importing interface translations'), 2631 'file' => './includes/locale.inc', 2632 // This is not a batch API construct, but data passed along to the 2633 // installer, so we know what did we import already. 2634 '#components' => $components, 2635 ); 2636 if (isset($finished)) { 2637 $batch['finished'] = $finished; 2638 } 2639 return $batch; 2640 } 2641 return FALSE; 2642 } 2643 2644 /** 2645 * Perform interface translation import as a batch step. 2646 * 2647 * @param $filepath 2648 * Path to a file to import. 2649 * @param $results 2650 * Contains a list of files imported. 2651 */ 2652 function _locale_batch_import($filepath, &$context) { 2653 // The filename is either {langcode}.po or {prefix}.{langcode}.po, so 2654 // we can extract the language code to use for the import from the end. 2655 if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { 2656 $file = (object) array('filename' => basename($filepath), 'filepath' => $filepath); 2657 _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]); 2658 $context['results'][] = $filepath; 2659 } 2660 } 2661 2662 /** 2663 * Finished callback of system page locale import batch. 2664 * Inform the user of translation files imported. 2665 */ 2666 function _locale_batch_system_finished($success, $results) { 2667 if ($success) { 2668 drupal_set_message(format_plural(count($results), 'One translation file imported for the newly installed modules.', '@count translation files imported for the newly installed modules.')); 2669 } 2670 } 2671 2672 /** 2673 * Finished callback of language addition locale import batch. 2674 * Inform the user of translation files imported. 2675 */ 2676 function _locale_batch_language_finished($success, $results) { 2677 if ($success) { 2678 drupal_set_message(format_plural(count($results), 'One translation file imported for the enabled modules.', '@count translation files imported for the enabled modules.')); 2679 } 2680 } 2681 2682 /** 2683 * @} End of "locale-autoimport" 2684 */
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Thu Mar 24 11:18:33 2011 | Cross-referenced by PHPXref 0.7 |