[ Index ]

PHP Cross Reference of Drupal 6 (gatewave)

title

Body

[close]

/includes/ -> locale.inc (source)

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


Generated: Thu Mar 24 11:18:33 2011 Cross-referenced by PHPXref 0.7