<?php

/**
 * @file
 *   Add language handling functionality and enables the translation of the
 *   user interface to languages other than English.
 *
 *   When enabled, multiple languages can be set up. The site interface
 *   can be displayed in different languages, as well as nodes can have languages
 *   assigned. The setup of languages and translations is completely web based.
 *   Gettext portable object files are supported.
 */

/**
 * Regular expression pattern used to localize JavaScript strings.
 */
const LOCALE_JS_STRING = '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+';

/**
 * Regular expression pattern used to match simple JS object literal.
 *
 * This pattern matches a basic JS object, but will fail on an object with
 * nested objects. Used in JS file parsing for string arg processing.
 */
const LOCALE_JS_OBJECT = '\{.*?\}';

/**
 * Regular expression to match an object containing a key 'context'.
 *
 * Pattern to match a JS object containing a 'context key' with a string value,
 * which is captured. Will fail if there are nested objects.
 */
define('LOCALE_JS_OBJECT_CONTEXT', '
  \{              # match object literal start
  .*?             # match anything, non-greedy
  (?:             # match a form of "context"
    \'context\'
    |
    "context"
    |
    context
  )
  \s*:\s*         # match key-value separator ":"
  (' . LOCALE_JS_STRING . ')  # match context string
  .*?             # match anything, non-greedy
  \}              # match end of object literal
');

/**
 * Flag for locally not customized interface translation.
 *
 * Such translations are imported from .po files downloaded from
 * localize.drupal.org for example.
 */
const LOCALE_NOT_CUSTOMIZED = 0;

/**
 * Flag for locally customized interface translation.
 *
 * Such translations are edited from their imported originals on the user
 * interface or are imported as customized.
 */
const LOCALE_CUSTOMIZED = 1;

// ---------------------------------------------------------------------------------
// Hook implementations

/**
 * Implements hook_help().
 */
function locale_help($path, $arg) {
  switch ($path) {
    case 'admin/help#locale':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Locale module allows your Drupal site to be presented in languages other than the default English, and to be multilingual. The Locale module works by maintaining a database of translations, and examining text as it is about to be displayed. When a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for review by a translator. For more information, see the online handbook entry for <a href="@locale">Locale module</a>.', array('@locale' => 'http://drupal.org/documentation/modules/locale/')) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Translating interface text') . '</dt>';
      $output .= '<dd>' . t('Translations of text in the Drupal interface may be provided by:');
      $output .= '<ul>';
      $output .= '<li>' . t("Translating within your site, using the Locale module's integrated <a href='@translate'>translation interface</a>.", array('@translate' => url('admin/config/regional/translate'))) . '</li>';
      $output .= '<li>' . t('Importing files from a set of existing translations, known as a translation package. A translation package enables the display of a specific version of Drupal in a specific language, and contains files in the Gettext Portable Object (<em>.po</em>) format. Although not all languages are available for every version of Drupal, translation packages for many languages are available for download from the <a href="@translations">Drupal translations page</a>.', array('@translations' => 'http://localize.drupal.org')) . '</li>';
      $output .= '<li>' . t("If an existing translation package does not meet your needs, the Gettext Portable Object (<em>.po</em>) files within a package may be modified, or new <em>.po</em> files may be created, using a desktop Gettext editor. The Locale module's <a href='@import'>import</a> feature allows the translated strings from a new or modified <em>.po</em> file to be added to your site. The Locale module's <a href='@export'>export</a> feature generates files from your site's translated strings, that can either be shared with others or edited offline by a Gettext translation editor.", array('@import' => url('admin/config/regional/translate/import'), '@export' => url('admin/config/regional/translate/export'))) . '</li>';
      $output .= '</ul></dd>';
      $output .= '</dl>';
      return $output;

    case 'admin/config/regional/language':
      return '<p>' . t('Interface text can be translated. <a href="@translations">Download contributed translations</a> from Drupal.org.', array('@translations' => 'http://localize.drupal.org')) . '</p>';

    case 'admin/config/regional/translate':
      $output = '<p>' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: For translation tasks involving many strings, it may be more convenient to <a href="@export">export</a> strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings in a specific language.', array('@export' => url('admin/config/regional/translate/export'))) . '</p>';
      return $output;

    case 'admin/config/regional/translate/import':
      $output = '<p>' . t('This page imports the translated strings contained in an individual Gettext Portable Object (<em>.po</em>) file. Normally distributed as part of a translation package (each translation package may contain several <em>.po</em> files), a <em>.po</em> file may need to be imported after offline editing in a Gettext translation editor. Importing an individual <em>.po</em> file may be a lengthy process.') . '</p>';
      $output .= '<p>' . t('Note that the <em>.po</em> files within a translation package are imported automatically (if available) when new modules or themes are enabled, or as new languages are added. Since this page only allows the import of one <em>.po</em> file at a time, it may be simpler to download and extract a translation package into your Drupal installation directory and <a href="@language-add">add the language</a> (which automatically imports all <em>.po</em> files within the package). Translation packages are available for download on the <a href="@translations">Drupal translation page</a>.', array('@language-add' => url('admin/config/regional/language/add'), '@translations' => 'http://localize.drupal.org')) . '</p>';
      return $output;

    case 'admin/config/regional/translate/export':
      return '<p>' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (<em>.po</em>) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (<em>.pot</em>) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '</p>';
  }
}

/**
 * Implements hook_menu().
 */
function locale_menu() {
  // Translation functionality.
  $items['admin/config/regional/translate'] = array(
    'title' => 'User interface translation',
    'description' => 'Translate the built-in user interface.',
    'page callback' => 'locale_translate_seek_screen',
    'access arguments' => array('translate interface'),
    'file' => 'locale.pages.inc',
    'weight' => -5,
  );
  $items['admin/config/regional/translate/translate'] = array(
    'title' => 'Translate',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/regional/translate/import'] = array(
    'title' => 'Import',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('locale_translate_import_form'),
    'access arguments' => array('translate interface'),
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
    'file' => 'locale.bulk.inc',
  );
  $items['admin/config/regional/translate/export'] = array(
    'title' => 'Export',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('locale_translate_export_form'),
    'access arguments' => array('translate interface'),
    'weight' => 30,
    'type' => MENU_LOCAL_TASK,
    'file' => 'locale.bulk.inc',
  );
  $items['admin/config/regional/translate/edit/%'] = array(
    'title' => 'Edit string',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('locale_translate_edit_form', 5),
    'access arguments' => array('translate interface'),
    'file' => 'locale.pages.inc',
  );
  $items['admin/config/regional/translate/delete/%'] = array(
    'title' => 'Delete string',
    'page callback' => 'locale_translate_delete_page',
    'page arguments' => array(5),
    'access arguments' => array('translate interface'),
    'file' => 'locale.pages.inc',
  );

  // Localize date formats.
  $items['admin/config/regional/date-time/locale'] = array(
    'title' => 'Localize',
    'description' => 'Configure date formats for each locale',
    'page callback' => 'locale_date_format_language_overview_page',
    'access arguments' => array('administer site configuration'),
    'type' => MENU_LOCAL_TASK,
    'weight' => -8,
    'file' => 'locale.admin.inc',
  );
  $items['admin/config/regional/date-time/locale/%/edit'] = array(
    'title' => 'Localize date formats',
    'description' => 'Configure date formats for each locale',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('locale_date_format_form', 5),
    'access arguments' => array('administer site configuration'),
    'file' => 'locale.admin.inc',
  );
  $items['admin/config/regional/date-time/locale/%/reset'] = array(
    'title' => 'Reset date formats',
    'description' => 'Reset localized date formats to global defaults',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('locale_date_format_reset_form', 5),
    'access arguments' => array('administer site configuration'),
    'file' => 'locale.admin.inc',
  );

  return $items;
}

/**
 * Implements hook_init().
 *
 * Initialize date formats according to the user's current locale.
 */
function locale_init() {
  global $conf, $language_interface;

  // For each date type (e.g. long, short), get the localized date format
  // for the user's current language and override the default setting for it
  // in $conf. This should happen on all pages except the date and time formats
  // settings page, where we want to display the site default and not the
  // localized version.
  if (strpos(current_path(), 'admin/config/regional/date-time/formats') !== 0) {
    $languages = array($language_interface->langcode);

    // Setup appropriate date formats for this locale.
    $formats = locale_get_localized_date_format($languages);
    foreach ($formats as $format_type => $format) {
      $conf[$format_type] = $format;
    }
  }
}

/**
 * Implements hook_permission().
 */
function locale_permission() {
  return array(
    'translate interface' => array(
      'title' => t('Translate interface texts'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function locale_form_node_form_alter(&$form, &$form_state) {
  $form['#submit'][] = 'locale_field_node_form_submit';
}

/**
 * Form submit handler for node_form().
 *
 * Checks if Locale is registered as a translation handler and handle possible
 * node language changes.
 *
 * This submit handler needs to run before entity_form_submit_build_entity()
 * is invoked by node_form_submit_build_node(), because it alters the values of
 * attached fields. Therefore, it cannot be a hook_node_submit() implementation.
 */
function locale_field_node_form_submit($form, &$form_state) {
  if (field_has_translation_handler('node', 'locale')) {
    $bundle = $form_state['values']['type'];
    $node_language = $form_state['values']['langcode'];

    foreach (field_info_instances('node', $bundle) as $instance) {
      $field_name = $instance['field_name'];
      $field = field_info_field($field_name);
      $previous_langcode = $form[$field_name]['#language'];

      // Handle a possible language change: new language values are inserted,
      // previous ones are deleted.
      if ($field['translatable'] && $previous_langcode != $node_language) {
        $form_state['values'][$field_name][$node_language] = $form_state['values'][$field_name][$previous_langcode];
        $form_state['values'][$field_name][$previous_langcode] = array();
      }
    }
  }
}

/**
 * Implements hook_theme().
 */
function locale_theme() {
  return array(
    'locale_date_format_form' => array(
      'render element' => 'form',
    ),
  );
}

/**
 * Implements hook_field_language_alter().
 */
function locale_field_language_alter(&$display_langcode, $context) {
  // Do not apply core language fallback rules if they are disabled or if Locale
  // is not registered as a translation handler.
  if (variable_get('locale_field_language_fallback', TRUE) && field_has_translation_handler($context['entity_type'], 'locale')) {
    locale_field_language_fallback($display_langcode, $context['entity'], $context['langcode']);
  }
}

/**
 * Applies language fallback rules to the fields attached to the given entity.
 *
 * Core language fallback rules simply check if fields have a field translation
 * for the requested language code. If so the requested language is returned,
 * otherwise all the fallback candidates are inspected to see if there is a
 * field translation available in another language.
 * By default this is called by locale_field_language_alter(), but this
 * behavior can be disabled by setting the 'locale_field_language_fallback'
 * variable to FALSE.
 *
 * @param $field_langcodes
 *   A reference to an array of language codes keyed by field name.
 * @param $entity
 *   The entity to be displayed.
 * @param $langcode
 *   The language code $entity has to be displayed in.
 */
function locale_field_language_fallback(&$field_langcodes, $entity, $langcode) {
  // Lazily init fallback candidates to avoid unnecessary calls.
  $fallback_candidates = NULL;
  $field_languages = array();

  foreach ($field_langcodes as $field_name => $field_langcode) {
    // If the requested language is defined for the current field use it,
    // otherwise search for a fallback value among the fallback candidates.
    if (isset($entity->{$field_name}[$langcode])) {
      $field_langcodes[$field_name] = $langcode;
    }
    elseif (!empty($entity->{$field_name})) {
      if (!isset($fallback_candidates)) {
        require_once DRUPAL_ROOT . '/core/includes/language.inc';
        $fallback_candidates = language_fallback_get_candidates();
      }
      foreach ($fallback_candidates as $fallback_langcode) {
        if (isset($entity->{$field_name}[$fallback_langcode])) {
          $field_langcodes[$field_name] = $fallback_langcode;
          break;
        }
      }
    }
  }
}

/**
 * Implements hook_entity_info_alter().
 */
function locale_entity_info_alter(&$entity_info) {
  $entity_info['node']['translation']['locale'] = TRUE;
}

/**
 * Implements hook_language_insert().
 */
function locale_language_insert($language) {
  // @todo move these two cache clears out. See http://drupal.org/node/1293252
  // Changing the language settings impacts the interface.
  cache('page')->flush();
  // Force JavaScript translation file re-creation for the new language.
  _locale_invalidate_js($language->langcode);
}

/**
 * Implements hook_language_update().
 */
function locale_language_update($language) {
  // @todo move these two cache clears out. See http://drupal.org/node/1293252
  // Changing the language settings impacts the interface.
  cache('page')->flush();
  // Force JavaScript translation file re-creation for the modified language.
  _locale_invalidate_js($language->langcode);
}

/**
 * Implements hook_language_delete().
 */
function locale_language_delete($language) {
  // Remove translations.
  db_delete('locales_target')
    ->condition('language', $language->langcode)
    ->execute();

  _locale_invalidate_js($language->langcode);

  // Changing the language settings impacts the interface:
  cache('page')->flush();

  // Clearing all locale cache from database
  cache()->delete('locale:' . $language->langcode);
}


// ---------------------------------------------------------------------------------
// Locale core functionality

/**
 * Provides interface translation services.
 *
 * This function is called from t() to translate a string if needed.
 *
 * @param $string
 *   A string to look up translation for. If omitted, all the
 *   cached strings will be returned in all languages already
 *   used on the page.
 * @param $context
 *   The context of this string.
 * @param $langcode
 *   Language code to use for the lookup.
 */
function locale($string = NULL, $context = NULL, $langcode = NULL) {
  global $language_interface;

  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['locale'] = &drupal_static(__FUNCTION__);
  }
  $locale_t = &$drupal_static_fast['locale'];


  if (!isset($string)) {
    // Return all cached strings if no string was specified
    return $locale_t;
  }

  $langcode = isset($langcode) ? $langcode : $language_interface->langcode;

  // Store database cached translations in a static variable. Only build the
  // cache after $language_interface has been set to avoid an unnecessary cache
  // rebuild.
  if (!isset($locale_t[$langcode]) && isset($language_interface)) {
    $locale_t[$langcode] = array();
    // Disabling the usage of string caching allows a module to watch for
    // the exact list of strings used on a page. From a performance
    // perspective that is a really bad idea, so we have no user
    // interface for this. Be careful when turning this option off!
    if (variable_get('locale_cache_strings', 1) == 1) {
      if ($cache = cache()->get('locale:' . $langcode)) {
        $locale_t[$langcode] = $cache->data;
      }
      elseif (lock_acquire('locale_cache_' . $langcode)) {
        // Refresh database stored cache of translations for given language.
        // We only store short strings used in current version, to improve
        // performance and consume less memory.
        $result = db_query("SELECT s.source, s.context, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.version = :version AND LENGTH(s.source) < :length", array(':language' => $langcode, ':version' => VERSION, ':length' => variable_get('locale_cache_length', 75)));
        foreach ($result as $data) {
          $locale_t[$langcode][$data->context][$data->source] = (empty($data->translation) ? TRUE : $data->translation);
        }
        cache()->set('locale:' . $langcode, $locale_t[$langcode]);
        lock_release('locale_cache_' . $langcode);
      }
    }
  }

  // If we have the translation cached, skip checking the database
  if (!isset($locale_t[$langcode][$context][$string])) {

    // We do not have this translation cached, so get it from the DB.
    $translation = db_query("SELECT s.lid, t.translation, s.version FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context", array(
      ':language' => $langcode,
      ':source' => $string,
      ':context' => (string) $context,
    ))->fetchObject();
    if ($translation) {
      // We have the source string at least.
      // Cache translation string or TRUE if no translation exists.
      $locale_t[$langcode][$context][$string] = (empty($translation->translation) ? TRUE : $translation->translation);

      if ($translation->version != VERSION) {
        // This is the first use of this string under current Drupal version. Save version
        // and clear cache, to include the string into caching next time. Saved version is
        // also a string-history information for later pruning of the tables.
        db_update('locales_source')
          ->fields(array('version' => VERSION))
          ->condition('lid', $translation->lid)
          ->execute();
        cache()->deletePrefix('locale:');
      }
    }
    else {
      // We don't have the source string, cache this as untranslated.
      db_merge('locales_source')
        ->insertFields(array(
          'location' => request_uri(),
          'version' => VERSION,
        ))
        ->key(array(
          'source' => $string,
          'context' => (string) $context,
        ))
        ->execute();
      $locale_t[$langcode][$context][$string] = TRUE;
      // Clear locale cache so this string can be added in a later request.
      cache()->deletePrefix('locale:');
    }
  }

  return ($locale_t[$langcode][$context][$string] === TRUE ? $string : $locale_t[$langcode][$context][$string]);
}

/**
 * Reset static variables used by locale().
 */
function locale_reset() {
  drupal_static_reset('locale');
}

/**
 * Returns plural form index for a specific number.
 *
 * The index is computed from the formula of this language.
 *
 * @param $count
 *   Number to return plural for.
 * @param $langcode
 *   Optional language code to translate to a language other than
 *   what is used to display the page.
 * @return
 *   The numeric index of the plural variant to use for this $langcode and
 *   $count combination or -1 if the language was not found or does not have a
 *   plural formula.
 */
function locale_get_plural($count, $langcode = NULL) {
  global $language_interface;

  // Used to locally cache the plural formulas for all languages.
  $plural_formulas = &drupal_static(__FUNCTION__, array());
  // Used to store precomputed plural indexes corresponding to numbers
  // individually for each language.
  $plural_indexes = &drupal_static(__FUNCTION__ . ':plurals', array());

  $langcode = $langcode ? $langcode : $language_interface->langcode;

  if (!isset($plural_indexes[$langcode][$count])) {
    // Retrieve and statically cache the plural formulas for all languages.
    if (empty($plural_formulas)) {
      $plural_formulas = variable_get('locale_translation_plurals', array());
    }
    // If there is a plural formula for the language, evaluate it for the given
    // $count and statically cache the result for the combination of language
    // and count, since the result will always be identical.
    if (!empty($plural_formulas[$langcode])) {
      // $n is used inside the expression in the eval().
      $n = $count;
      $plural_indexes[$langcode][$count] = @eval('return intval(' . $plural_formulas[$langcode]['formula'] . ');');
    }
    // In case there is no plural formula for English (no imported translation
    // for English), use a default formula.
    elseif ($langcode == 'en') {
      $plural_indexes[$langcode][$count] = (int) ($count != 1);
    }
    // Otherwise, return -1 (unknown).
    else {
      $plural_indexes[$langcode][$count] = -1;
    }
  }
  return $plural_indexes[$langcode][$count];
}


/**
 * Implements hook_modules_installed().
 */
function locale_modules_installed($modules) {
  locale_system_update($modules);
}

/**
 * Implements hook_themes_enabled().
 *
 * @todo This is technically wrong. We must not import upon enabling, but upon
 *   initial installation. The theme system is missing an installation hook.
 */
function locale_themes_enabled($themes) {
  locale_system_update($themes);
}

/**
 * Imports translations when new modules or themes are installed.
 *
 * This function will either import translation for the component change
 * right away, or start a batch if more files need to be imported.
 *
 * @param $components
 *   An array of component (theme and/or module) names to import
 *   translations for.
 *
 * @todo
 *   This currently imports all .po files available, independent of
 *   $components. Once we integrated with update status for project
 *   identification, we should revisit and only import files for the
 *   identified projects for the components.
 */
function locale_system_update($components) {
  include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
  if ($batch = locale_translate_batch_import_files(NULL, TRUE)) {
    batch_set($batch);
  }
}

/**
 * Implements hook_js_alter().
 *
 * This function checks all JavaScript files currently added via drupal_add_js()
 * and invokes parsing if they have not yet been parsed for Drupal.t()
 * and Drupal.formatPlural() calls. Also refreshes the JavaScript translation
 * file if necessary, and adds it to the page.
 */
function locale_js_alter(&$javascript) {
  global $language_interface;

  $dir = 'public://' . variable_get('locale_js_directory', 'languages');
  $parsed = variable_get('javascript_parsed', array());
  $files = $new_files = FALSE;

  foreach ($javascript as $item) {
    if ($item['type'] == 'file') {
      $files = TRUE;
      $filepath = $item['data'];
      if (!in_array($filepath, $parsed)) {
        // Don't parse our own translations files.
        if (substr($filepath, 0, strlen($dir)) != $dir) {
          _locale_parse_js_file($filepath);
          $parsed[] = $filepath;
          $new_files = TRUE;
        }
      }
    }
  }

  // If there are any new source files we parsed, invalidate existing
  // JavaScript translation files for all languages, adding the refresh
  // flags into the existing array.
  if ($new_files) {
    $parsed += _locale_invalidate_js();
  }

  // If necessary, rebuild the translation file for the current language.
  if (!empty($parsed['refresh:' . $language_interface->langcode])) {
    // Don't clear the refresh flag on failure, so that another try will
    // be performed later.
    if (_locale_rebuild_js()) {
      unset($parsed['refresh:' . $language_interface->langcode]);
    }
    // Store any changes after refresh was attempted.
    variable_set('javascript_parsed', $parsed);
  }
  // If no refresh was attempted, but we have new source files, we need
  // to store them too. This occurs if current page is in English.
  elseif ($new_files) {
    variable_set('javascript_parsed', $parsed);
  }

  // Add the translation JavaScript file to the page.
  $locale_javascripts = variable_get('locale_translation_javascript', array());
  if ($files && !empty($locale_javascripts[$language_interface->langcode])) {
    // Add the translation JavaScript file to the page.
    $file = $dir . '/' . $language_interface->langcode . '_' . $locale_javascripts[$language_interface->langcode] . '.js';
    $javascript[$file] = drupal_js_defaults($file);
  }
}

 /**
 * Implement hook_library_info_alter().
 *
 * Provides the language support for the jQuery UI Date Picker.
 */
function locale_library_info_alter(&$libraries, $module) {
  global $language_interface;
  if ($module == 'system' && isset($libraries['system']['ui.datepicker'])) {
    $datepicker = drupal_get_path('module', 'locale') . '/locale.datepicker.js';
    $libraries['system']['ui.datepicker']['js'][$datepicker] = array('group' => JS_THEME);
    $libraries['system']['ui.datepicker']['js'][] = array(
      'data' => array(
        'jqueryuidatepicker' => array(
          'rtl' => $language_interface->direction == LANGUAGE_RTL,
          'firstDay' => variable_get('date_first_day', 0),
        ),
      ),
      'type' => 'setting',
    );
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for language_admin_overview_form().
 */
function locale_form_language_admin_overview_form_alter(&$form, &$form_state) {
  $languages = $form['languages']['#languages'];

  $total_strings = db_query("SELECT COUNT(*) FROM {locales_source}")->fetchField();
  $stats = array_fill_keys(array_keys($languages), array());

  // If we have source strings, count translations and calculate progress.
  if (!empty($total_strings)) {
    $translations = db_query("SELECT COUNT(*) AS translated, t.language FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY language");
    foreach ($translations as $data) {
      $stats[$data->language]['translated'] = $data->translated;
      if ($data->translated > 0) {
        $stats[$data->language]['ratio'] = round($data->translated / $total_strings * 100, 2);
      }
    }
  }

  array_splice($form['languages']['#header'], -1, 0, t('Interface translation'));

  foreach ($languages as $langcode => $language) {
    $stats[$langcode] += array(
      'translated' => 0,
      'ratio' => 0,
    );
    if ($langcode != 'en' || locale_translate_english()) {
      $form['languages'][$langcode]['locale_statistics'] = array(
        '#type' => 'link',
        '#title' => t('@translated/@total (@ratio%)', array(
          '@translated' => $stats[$langcode]['translated'],
          '@total' => $total_strings,
          '@ratio' => $stats[$langcode]['ratio'],
        )),
        '#href' => 'admin/config/regional/translate/translate',
      );
    }
    else {
      $form['languages'][$langcode]['locale_statistics'] = array(
        '#markup' => t('not applicable'),
      );
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for language_admin_add_form(().
 */
function locale_form_language_admin_add_form_alter(&$form, &$form_state) {
  $form['predefined_submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
  $form['custom_language']['submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
}

/**
 * Set a batch for newly added language.
 */
function locale_form_language_admin_add_form_alter_submit($form, $form_state) {
  if (empty($form_state['values']['predefined_langcode']) || $form_state['values']['predefined_langcode'] == 'custom') {
    $langcode = $form_state['values']['langcode'];
  }
  else {
    $langcode = $form_state['values']['predefined_langcode'];
  }

  include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
  locale_translate_add_language_set_batch($langcode);
}

/**
 * Implements hook_form_FORM_ID_alter() for language_admin_edit_form().
 */
function locale_form_language_admin_edit_form_alter(&$form, &$form_state) {
  if ($form['langcode']['#type'] == 'value' && $form['langcode']['#value'] == 'en') {
    $form['locale_translate_english'] = array(
      '#title' => t('Enable interface translation to English'),
      '#type' => 'checkbox',
      '#default_value' => locale_translate_english(),
    );
    $form['#submit'][] = 'locale_form_language_admin_edit_form_alter_submit';
  }
}

/**
 * Submission handler to record our custom setting.
 */
function locale_form_language_admin_edit_form_alter_submit($form, $form_state) {
  variable_set('locale_translate_english', $form_state['values']['locale_translate_english']);
}

/**
 * Utility function to tell if locale translates to English.
 */
function locale_translate_english() {
  return variable_get('locale_translate_english', FALSE);
}

/**
 * Implements hook_form_FORM_ID_alter() for system_file_system_settings().
 *
 * Add interface translation directory setting to directories configuration.
 */
function locale_form_system_file_system_settings_alter(&$form, $form_state) {
  $form['locale_translate_file_directory'] = array(
    '#type' => 'textfield',
    '#title' => t('Interface translations directory'),
    '#default_value' => variable_get('locale_translate_file_directory', conf_path() . '/files/translations'),
    '#maxlength' => 255,
    '#description' => t('A local file system path where interface translation files are looked for. This directory must exist.'),
    '#after_build' => array('system_check_directory'),
    '#weight' => 10,
  );
  if ($form['file_default_scheme']) {
    $form['file_default_scheme']['#weight'] = 20;
  }
}

/**
 * Implements hook_preprocess_HOOK() for node.tpl.php.
 */
function locale_preprocess_node(&$variables) {
  if ($variables['langcode'] != LANGUAGE_NOT_SPECIFIED) {
    global $language_interface;

    $node_language = language_load($variables['langcode']);
    if ($node_language->langcode != $language_interface->langcode) {
      // If the node language was different from the page language, we should
      // add markup to identify the language. Otherwise the page language is
      // inherited.
      $variables['attributes_array']['lang'] = $variables['langcode'];
      if ($node_language->direction != $language_interface->direction) {
        // If text direction is different form the page's text direction, add
        // direction information as well.
        $dir = array('ltr', 'rtl');
        $variables['attributes_array']['dir'] = $dir[$node_language->direction];
      }
    }
  }
}

/**
 * Check that a string is safe to be added or imported as a translation.
 *
 * This test can be used to detect possibly bad translation strings. It should
 * not have any false positives. But it is only a test, not a transformation,
 * as it destroys valid HTML. We cannot reliably filter translation strings
 * on import because some strings are irreversibly corrupted. For example,
 * a &amp; in the translation would get encoded to &amp;amp; by filter_xss()
 * before being put in the database, and thus would be displayed incorrectly.
 *
 * The allowed tag list is like filter_xss_admin(), but omitting div and img as
 * not needed for translation and likely to cause layout issues (div) or a
 * possible attack vector (img).
 */
function locale_string_is_safe($string) {
  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')));
}

/**
 * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and
 * Drupal.formatPlural() and inserts them into the database.
 */
function _locale_parse_js_file($filepath) {
  // The file path might contain a query string, so make sure we only use the
  // actual file.
  $parsed_url = drupal_parse_url($filepath);
  $filepath = $parsed_url['path'];
  // Load the JavaScript file.
  $file = file_get_contents($filepath);

  // Match all calls to Drupal.t() in an array.
  // Note: \s also matches newlines with the 's' modifier.
  preg_match_all('~
    [^\w]Drupal\s*\.\s*t\s*                       # match "Drupal.t" with whitespace
    \(\s*                                         # match "(" argument list start
    (' . LOCALE_JS_STRING . ')\s*                 # capture string argument
    (?:,\s*' . LOCALE_JS_OBJECT . '\s*            # optionally capture str args
      (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context
    ?)?                                           # close optional args
    [,\)]                                         # match ")" or "," to finish
    ~sx', $file, $t_matches);

  // Match all Drupal.formatPlural() calls in another array.
  preg_match_all('~
    [^\w]Drupal\s*\.\s*formatPlural\s*  # match "Drupal.formatPlural" with whitespace
    \(                                  # match "(" argument list start
    \s*.+?\s*,\s*                       # match count argument
    (' . LOCALE_JS_STRING . ')\s*,\s*   # match singular string argument
    (                             # capture plural string argument
      (?:                         # non-capturing group to repeat string pieces
        (?:
          \'                      # match start of single-quoted string
          (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
          @count                  # match "@count"
          (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
          \'                      # match end of single-quoted string
          |
          "                       # match start of double-quoted string
          (?:\\\\"|[^"])*         # match any character except unescaped double-quote
          @count                  # match "@count"
          (?:\\\\"|[^"])*         # match any character except unescaped double-quote
          "                       # match end of double-quoted string
        )
        (?:\s*\+\s*)?             # match "+" with possible whitespace, for str concat
      )+                          # match multiple because we supports concatenating strs
    )\s*                          # end capturing of plural string argument
    (?:,\s*' . LOCALE_JS_OBJECT . '\s*          # optionally capture string args
      (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)?  # optionally capture context
    )?
    [,\)]
    ~sx', $file, $plural_matches);

  $matches = array();

  // Add strings from Drupal.t().
  foreach ($t_matches[1] as $key => $string) {
    $matches[] = array(
      'string'  => $string,
      'context' => $t_matches[2][$key],
    );
  }

  // Add string from Drupal.formatPlural().
  foreach ($plural_matches[1] as $key => $string) {
    $matches[] = array(
      'string'  => $string,
      'context' => $plural_matches[3][$key],
    );

    // If there is also a plural version of this string, add it to the strings array.
    if (isset($plural_matches[2][$key])) {
      $matches[] = array(
        'string'  => $plural_matches[2][$key],
        'context' => $plural_matches[3][$key],
      );
    }
  }

  // Loop through all matches and process them.
  foreach ($matches as $key => $match) {

    // Remove the quotes and string concatenations from the string and context.
    $string =  implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['string'], 1, -1)));
    $context = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['context'], 1, -1)));

    $source = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $string, ':context' => $context))->fetchObject();
    if ($source) {
      // We already have this source string and now have to add the location
      // to the location column, if this file is not yet present in there.
      $locations = preg_split('~\s*;\s*~', $source->location);

      if (!in_array($filepath, $locations)) {
        $locations[] = $filepath;
        $locations = implode('; ', $locations);

        // Save the new locations string to the database.
        db_update('locales_source')
          ->fields(array(
            'location' => $locations,
          ))
          ->condition('lid', $source->lid)
          ->execute();
      }
    }
    else {
      // We don't have the source string yet, thus we insert it into the database.
      db_insert('locales_source')
        ->fields(array(
          'location'  => $filepath,
          'source'    => $string,
          'context'   => $context,
        ))
        ->execute();
    }
  }
}

/**
 * Force the JavaScript translation file(s) to be refreshed.
 *
 * This function sets a refresh flag for a specified language, or all
 * languages except English, if none specified. JavaScript translation
 * files are rebuilt (with locale_update_js_files()) the next time a
 * request is served in that language.
 *
 * @param $langcode
 *   The language code for which the file needs to be refreshed.
 *
 * @return
 *   New content of the 'javascript_parsed' variable.
 */
function _locale_invalidate_js($langcode = NULL) {
  $parsed = variable_get('javascript_parsed', array());

  if (empty($langcode)) {
    // Invalidate all languages.
    $languages = language_list();
    if (!locale_translate_english()) {
      unset($languages['en']);
    }
    foreach ($languages as $lcode => $data) {
      $parsed['refresh:' . $lcode] = 'waiting';
    }
  }
  else {
    // Invalidate single language.
    $parsed['refresh:' . $langcode] = 'waiting';
  }

  variable_set('javascript_parsed', $parsed);
  return $parsed;
}

/**
 * (Re-)Creates the JavaScript translation file for a language.
 *
 * @param $langcode
 *   The language, the translation file should be (re)created for.
 */
function _locale_rebuild_js($langcode = NULL) {
  if (!isset($langcode)) {
    $language = drupal_container()->get(LANGUAGE_TYPE_INTERFACE);
  }
  else {
    // Get information about the locale.
    $languages = language_list();
    $language = $languages[$langcode];
  }

  // Construct the array for JavaScript translations.
  // Only add strings with a translation to the translations array.
  $result = db_query("SELECT s.lid, s.source, s.context, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%'", array(':language' => $language->langcode));

  $translations = array();
  foreach ($result as $data) {
    $translations[$data->context][$data->source] = $data->translation;
  }

  // Construct the JavaScript file, if there are translations.
  $data_hash = NULL;
  $data = $status = '';
  if (!empty($translations)) {

    $data = "Drupal.locale = { ";

    $locale_plurals = variable_get('locale_translation_plurals', array());
    if (!empty($locale_plurals[$language->langcode])) {
      $data .= "'pluralFormula': function (\$n) { return Number({$locale_plurals[$language->langcode]['formula']}); }, ";
    }

    $data .= "'strings': " . drupal_json_encode($translations) . " };";
    $data_hash = drupal_hash_base64($data);
  }

  // Construct the filepath where JS translation files are stored.
  // There is (on purpose) no front end to edit that variable.
  $dir = 'public://' . variable_get('locale_js_directory', 'languages');

  // Delete old file, if we have no translations anymore, or a different file to be saved.
  $locale_javascripts = variable_get('locale_translation_javascript', array());
  $changed_hash = !isset($locale_javascripts[$language->langcode]) || ($locale_javascripts[$language->langcode] != $data_hash);
  if (!empty($locale_javascripts[$language->langcode]) && (!$data || $changed_hash)) {
    file_unmanaged_delete($dir . '/' . $language->langcode . '_' . $locale_javascripts[$language->langcode] . '.js');
    $locale_javascripts[$language->langcode] = '';
    $status = 'deleted';
  }

  // Only create a new file if the content has changed or the original file got
  // lost.
  $dest = $dir . '/' . $language->langcode . '_' . $data_hash . '.js';
  if ($data && ($changed_hash || !file_exists($dest))) {
    // Ensure that the directory exists and is writable, if possible.
    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);

    // Save the file.
    if (file_unmanaged_save_data($data, $dest)) {
      $locale_javascripts[$language->langcode] = $data_hash;
      // If we deleted a previous version of the file and we replace it with a
      // new one we have an update.
      if ($status == 'deleted') {
        $status = 'updated';
      }
      // If the file did not exist previously and the data has changed we have
      // a fresh creation.
      elseif ($changed_hash) {
        $status = 'created';
      }
      // If the data hash is unchanged the translation was lost and has to be
      // rebuilt.
      else {
        $status = 'rebuilt';
      }
    }
    else {
      $locale_javascripts[$language->langcode] = '';
      $status = 'error';
    }
  }

  // Save the new JavaScript hash (or an empty value if the file just got
  // deleted). Act only if some operation was executed that changed the hash
  // code.
  if ($status && $changed_hash) {
    variable_set('locale_translation_javascript', $locale_javascripts);
  }

  // Log the operation and return success flag.
  switch ($status) {
    case 'updated':
      watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => $language->name));
      return TRUE;
    case 'rebuilt':
      watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $locale_javascripts[$language->langcode]), WATCHDOG_WARNING);
      // Proceed to the 'created' case as the JavaScript translation file has
      // been created again.
    case 'created':
      watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => $language->name));
      return TRUE;
    case 'deleted':
      watchdog('locale', 'Removed JavaScript translation file for the language %language because no translations currently exist for that language.', array('%language' => $language->name));
      return TRUE;
    case 'error':
      watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => $language->name), WATCHDOG_ERROR);
      return FALSE;
    default:
      // No operation needed.
      return TRUE;
  }
}

/**
 * Save locale specific date formats to the database.
 *
 * @param $langcode
 *   Language code, can be 2 characters, e.g. 'en' or 5 characters, e.g.
 *   'en-CA'.
 * @param $type
 *   Date format type, e.g. 'short', 'medium'.
 * @param $format
 *   The date format string.
 */
function locale_date_format_save($langcode, $type, $format) {
  $locale_format = array();
  $locale_format['language'] = $langcode;
  $locale_format['type'] = $type;
  $locale_format['format'] = $format;

  $is_existing = (bool) db_query_range('SELECT 1 FROM {date_format_locale} WHERE language = :langcode AND type = :type', 0, 1, array(':langcode' => $langcode, ':type' => $type))->fetchField();
  if ($is_existing) {
    $keys = array('type', 'language');
    drupal_write_record('date_format_locale', $locale_format, $keys);
  }
  else {
    drupal_write_record('date_format_locale', $locale_format);
  }
}

/**
 * Select locale date format details from database.
 *
 * @param $languages
 *   An array of language codes.
 *
 * @return
 *   An array of date formats.
 */
function locale_get_localized_date_format($languages) {
  $formats = array();

  // Get list of different format types.
  $format_types = system_get_date_types();
  $short_default = variable_get('date_format_short', 'm/d/Y - H:i');

  // Loop through each language until we find one with some date formats
  // configured.
  foreach ($languages as $language) {
    $date_formats = system_date_format_locale($language);
    if (!empty($date_formats)) {
      // We have locale-specific date formats, so check for their types. If
      // we're missing a type, use the default setting instead.
      foreach ($format_types as $type => $type_info) {
        // If format exists for this language, use it.
        if (!empty($date_formats[$type])) {
          $formats['date_format_' . $type] = $date_formats[$type];
        }
        // Otherwise get default variable setting. If this is not set, default
        // to the short format.
        else {
          $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
        }
      }

      // Return on the first match.
      return $formats;
    }
  }

  // No locale specific formats found, so use defaults.
  $system_types = array('short', 'medium', 'long');
  // Handle system types separately as they have defaults if no variable exists.
  $formats['date_format_short'] = $short_default;
  $formats['date_format_medium'] = variable_get('date_format_medium', 'D, m/d/Y - H:i');
  $formats['date_format_long'] = variable_get('date_format_long', 'l, F j, Y - H:i');

  // For non-system types, get the default setting, otherwise use the short
  // format.
  foreach ($format_types as $type => $type_info) {
    if (!in_array($type, $system_types)) {
      $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
    }
  }

  return $formats;
}
