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