diff --git a/core/includes/gettext.inc b/core/includes/gettext.inc index 18c27ea..84c7f40 100644 --- a/core/includes/gettext.inc +++ b/core/includes/gettext.inc @@ -23,11 +23,15 @@ * Drupal file object corresponding to the PO file to import. * @param $langcode * Language code. - * @param $mode - * Should existing translations be replaced LOCALE_IMPORT_KEEP or - * LOCALE_IMPORT_OVERWRITE. + * @param $overwrite_options + * An associative array indicating what data should be overwritten, if any. + * - not_customized: not customized strings should be overwritten. + * - customized: customized strings should be overwritten. + * @param $customized + * Whether the strings being imported should be saved as customized. + * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. */ -function _locale_import_po($file, $langcode, $mode) { +function _locale_import_po($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { // Try to allocate enough time to parse and import the data. drupal_set_time_limit(240); @@ -38,7 +42,7 @@ function _locale_import_po($file, $langcode, $mode) { } // Get strings from file (returns on failure after a partial import, or on success) - $status = _locale_import_read_po('db-store', $file, $mode, $langcode); + $status = _locale_import_read_po('db-store', $file, $overwrite_options, $langcode, $customized); if ($status === FALSE) { // Error messages are set in _locale_import_read_po(). return FALSE; @@ -80,13 +84,17 @@ function _locale_import_po($file, $langcode, $mode) { * Storage operation type: db-store or mem-store. * @param $file * Drupal file object corresponding to the PO file to import. - * @param $mode - * Should existing translations be replaced LOCALE_IMPORT_KEEP or - * LOCALE_IMPORT_OVERWRITE. + * @param $overwrite_options + * An associative array indicating what data should be overwritten, if any. + * - not_customized: not customized strings should be overwritten. + * - customized: customized strings should be overwritten. * @param $lang * Language code. + * @param $customized + * Whether the strings being imported should be saved as customized. + * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. */ -function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) { +function _locale_import_read_po($op, $file, $overwrite_options = NULL, $lang = NULL, $customized = LOCALE_NOT_CUSTOMIZED) { // The file will get closed by PHP on returning from this function. $fd = fopen($file->uri, 'rb'); @@ -138,7 +146,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) { } elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { // We are currently in string token, close it out. - _locale_import_one_string($op, $current, $mode, $lang, $file); + _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); // Start a new entry for the comment. $current = array(); @@ -182,7 +190,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) { if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { // We are currently in a message string, close it out. - _locale_import_one_string($op, $current, $mode, $lang, $file); + _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); // Start a new context for the id. $current = array(); @@ -212,7 +220,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) { if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { // We are currently in a message, start a new one. - _locale_import_one_string($op, $current, $mode, $lang, $file); + _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); $current = array(); } elseif (!empty($current['msgctxt'])) { @@ -326,7 +334,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) { // End of PO file, closed out the last entry. if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - _locale_import_one_string($op, $current, $mode, $lang, $file); + _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); } elseif ($context != 'COMMENT') { _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno); @@ -360,16 +368,20 @@ function _locale_import_message($message, $file, $lineno = NULL) { * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'. * @param $value * Details of the string stored. - * @param $mode - * Should existing translations be replaced LOCALE_IMPORT_KEEP or - * LOCALE_IMPORT_OVERWRITE. + * @param $overwrite_options + * An associative array indicating what data should be overwritten, if any. + * - not_customized: not customized strings should be overwritten. + * - customized: customized strings should be overwritten. * @param $lang * Language to store the string in. * @param $file * Object representation of file being imported, only required when op is * 'db-store'. + * @param $customized + * Whether the strings being imported should be saved as customized. + * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. */ -function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL) { +function _locale_import_one_string($op, $value = NULL, $overwrite_options = NULL, $lang = NULL, $file = NULL, $customized = LOCALE_NOT_CUSTOMIZED) { $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0)); $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE); $strings = &drupal_static(__FUNCTION__ . ':strings', array()); @@ -395,7 +407,7 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL // If 'msgid' is empty, it means we got values for the header of the // file as per the structure of the Gettext format. $locale_plurals = variable_get('locale_translation_plurals', array()); - if (($mode != LOCALE_IMPORT_KEEP) || empty($locale_plurals[$lang]['plurals'])) { + if ((array_sum($overwrite_options) > 0) || empty($locale_plurals[$lang]['plurals'])) { // Since we only need to parse the header if we ought to update the // plural formula, only run this if we don't need to keep existing // data untouched or if we don't have an existing plural formula. @@ -432,7 +444,8 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL $value['msgid'], $value['msgstr'], $comments, - $mode + $overwrite_options, + $customized ); } } // end of db-store operation @@ -454,15 +467,34 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL * Translation to language specified in $langcode. * @param $location * Location value to save with source string. - * @param $mode - * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE. + * @param $overwrite_options + * An associative array indicating what data should be overwritten, if any. + * - not_customized: not customized strings should be overwritten. + * - customized: customized strings should be overwritten. + * @param $customized + * (optional) Whether the strings being imported should be saved as customized. + * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. * * @return * The string ID of the existing string modified or the new string added. */ -function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $mode) { +function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { + + // Initialize overwrite options if not set. + $overwrite_options += array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ); + $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $source, ':context' => $context))->fetchField(); + // See whether a translation exists already and is customized; + // we'll consider it customized if at least one of the plural forms is. + $existing_customized = NULL; + if ($lid) { + $existing_customized = db_query("SELECT MAX(customized) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField(); + } + if (!empty($translation)) { // Skip this string unless it passes a check for dangerous code. if (!locale_string_is_safe($translation)) { @@ -479,25 +511,25 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t ->condition('lid', $lid) ->execute(); - $exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField(); - - if (!$exists) { + if (!isset($existing_customized)) { // No translation in this language. db_insert('locales_target') ->fields(array( 'lid' => $lid, 'language' => $langcode, 'translation' => $translation, + 'customized' => $customized, )) ->execute(); $report['additions']++; } - elseif ($mode == LOCALE_IMPORT_OVERWRITE) { - // Translation exists, only overwrite if instructed. + elseif ($overwrite_options[$existing_customized ? 'customized' : 'not_customized']) { + // Translation exists, only overwrite if instructed. db_update('locales_target') ->fields(array( 'translation' => $translation, + 'customized' => $customized, )) ->condition('language', $langcode) ->condition('lid', $lid) @@ -521,13 +553,14 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t 'lid' => $lid, 'language' => $langcode, 'translation' => $translation, + 'customized' => $customized, )) ->execute(); $report['additions']++; } } - elseif ($mode == LOCALE_IMPORT_OVERWRITE) { + elseif (isset($existing_customized) && $overwrite_options[$existing_customized ? 'customized' : 'not_customized']) { // Empty translation, remove existing if instructed. db_delete('locales_target') ->condition('language', $langcode) @@ -828,17 +861,67 @@ function _locale_import_parse_quoted($string) { * @param $language * Language object to generate the output for, or NULL if generating * translation template. + * @param $options + * (optional) An associative array specifying what to include in the output: + * - customized: include customized strings (if TRUE) + * - uncustomized: include non-customized string (if TRUE) + * - untranslated: include untranslated source strings (if TRUE) + * Ignored if $language is NULL. * * @return * An array of translated strings that can be used to generate an export. */ -function _locale_export_get_strings($language = NULL) { - if (isset($language)) { - $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language", array(':language' => $language->langcode)); +function _locale_export_get_strings($language = NULL, $options = array()) { + + // Assume FALSE for all options if not provided by the API. + $options += array( + 'customized' => FALSE, + 'not_customized' => FALSE, + 'not_translated' => FALSE, + ); + if (array_sum($options) == 0) { + // If user asked to not include anything in the translation files, + // that would not make sense, so just fall back on providing a template. + $language = NULL; + } + + // Build and execute query to collect source strings and translations. + $query = db_select('locales_source', 's'); + if (!empty($language)) { + if ($options['not_translated']) { + // Left join to keep untranslated strings in. + $query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode)); + } + else { + // Inner join to filter for only translations. + $query->innerJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode)); + } + if ($options['customized']) { + if (!$options['not_customized']) { + // Filter for customized strings only. + $query->condition('t.customized', LOCALE_CUSTOMIZED); + } + // Else no filtering needed in this case. + } + else { + if ($options['not_customized']) { + // Filter for non-customized strings only. + $query->condition('t.customized', LOCALE_NOT_CUSTOMIZED); + } + else { + // Filter for strings without translation. + $query->isNull('t.translation'); + } + } + $query->fields('t', array('translation')); } else { - $result = db_query("SELECT s.lid, s.source, s.context, s.location FROM {locales_source} s"); + $query->leftJoin('locales_target', 't', 's.lid = t.lid'); } + $query->fields('s', array('lid', 'source', 'context', 'location')); + $result = $query->execute(); + + // Structure results in an array with metainformation on the strings. $strings = array(); foreach ($result as $child) { $strings[$child->lid] = array( diff --git a/core/includes/locale.inc b/core/includes/locale.inc index 1f9567b..45e4a95 100644 --- a/core/includes/locale.inc +++ b/core/includes/locale.inc @@ -73,26 +73,28 @@ define('LOCALE_JS_OBJECT_CONTEXT', ' '); /** - * Translation import mode overwriting all existing translations - * if new translated version available. + * Flag for locally not customized interface translation. + * + * Such translations are imported from .po files downloaded from + * localize.drupal.org for example. */ -const LOCALE_IMPORT_OVERWRITE = 0; +const LOCALE_NOT_CUSTOMIZED = 0; /** - * Translation import mode keeping existing translations and only - * inserting new strings. + * 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_IMPORT_KEEP = 1; +const LOCALE_CUSTOMIZED = 1; /** - * URL language negotiation: use the path prefix as URL language - * indicator. + * URL language negotiation: use the path prefix as URL language indicator. */ const LANGUAGE_NEGOTIATION_URL_PREFIX = 0; /** - * URL language negotiation: use the domain as URL language - * indicator. + * URL language negotiation: use the domain as URL language indicator. */ const LANGUAGE_NEGOTIATION_URL_DOMAIN = 1; diff --git a/core/modules/block/block.js b/core/modules/block/block.js index 72b5673..67cbb31 100644 --- a/core/modules/block/block.js +++ b/core/modules/block/block.js @@ -32,6 +32,17 @@ Drupal.behaviors.blockSettingsSummary = { return vals.join(', '); }); + $('fieldset#edit-langcode', context).drupalSetSummary(function (context) { + var vals = []; + $('input[type="checkbox"]:checked', context).each(function () { + vals.push($.trim($(this).next('label').text())); + }); + if (!vals.length) { + vals.push(Drupal.t('Not restricted')); + } + return vals.join(', '); + }); + $('fieldset#edit-role', context).drupalSetSummary(function (context) { var vals = []; $('input[type="checkbox"]:checked', context).each(function () { diff --git a/core/modules/dashboard/dashboard.module b/core/modules/dashboard/dashboard.module index 889af83..3244de0 100644 --- a/core/modules/dashboard/dashboard.module +++ b/core/modules/dashboard/dashboard.module @@ -183,17 +183,28 @@ function dashboard_page_build(&$page) { ->condition('region', $region) ->fields('block') ->execute(); - foreach ($block_list as $block) { - if (!isset($blocks_found[$block->module . '_' . $block->delta])) { - $block->enabled = $block->page_match = TRUE; - $block->content = array('#markup' => '
(empty)
'); - if (!isset($block_info[$block->module])) { - $block_info[$block->module] = module_invoke($block->module, 'block_info'); + //Determine blocks that have been disabled (set empty) for langcodes visibility settings. + global $language_interface; + $result = db_query('SELECT module, delta, langcode FROM {block_langcode}'); + $block_langcodes = array(); + foreach ($result as $record) { + $block_langcodes[$record->module][$record->delta][$record->langcode] = TRUE; + } + foreach ($block_list as $key => $block) { + // This block should not be displayed with the active language, remove + // from display. + if(isset($block_langcodes[$block->module][$block->delta][$language_interface->langcode])) { + if (!isset($blocks_found[$block->module . '_' . $block->delta])) { + $block->enabled = $block->page_match = TRUE; + $block->content = array('#markup' => '
(empty)
'); + if (!isset($block_info[$block->module])) { + $block_info[$block->module] = module_invoke($block->module, 'block_info'); + } + $block->subject = t('@title', array('@title' => $block_info[$block->module][$block->delta]['info'])); + $block_render = array($block->module . '_' . $block->delta => $block); + $build = _block_get_renderable_region($block_render); + $page['content']['dashboard'][$block->region][] = $build; } - $block->subject = t('@title', array('@title' => $block_info[$block->module][$block->delta]['info'])); - $block_render = array($block->module . '_' . $block->delta => $block); - $build = _block_get_renderable_region($block_render); - $page['content']['dashboard'][$block->region][] = $build; } } } diff --git a/core/modules/language/language.install b/core/modules/language/language.install index ecf637d..87a1606 100644 --- a/core/modules/language/language.install +++ b/core/modules/language/language.install @@ -71,6 +71,33 @@ function language_schema() { 'list' => array('weight', 'name'), ), ); + $schema['block_langcode'] = array( + 'description' => 'Sets up display criteria for blocks based on langcode', + 'fields' => array( + 'module' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'description' => "The block's origin module, from {block}.module.", + ), + 'delta' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'description' => "The block's unique delta within module, from {block}.delta.", + ), + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'description' => "The machine-readable name of this language from {language}.langcode.", + ), + ), + 'primary key' => array('module', 'delta', 'langcode'), + 'indexes' => array( + 'langcode' => array('langcode'), + ), + ); return $schema; } diff --git a/core/modules/language/language.module b/core/modules/language/language.module index 20f7c62..962b771 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -186,6 +186,17 @@ function language_delete($langcode) { } /** + * Implements hook_modules_uninstalled(). + * + * Cleans up {block_langcode} table from modules' blocks. + */ +function language_modules_uninstalled($modules) { + db_delete('block_langcode') + ->condition('module', $modules, 'IN') + ->execute(); +} + +/** * Implements hook_css_alter(). * * This function checks all CSS files currently added via drupal_add_css() and @@ -211,3 +222,95 @@ function language_css_alter(&$css) { } } } + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Adds per language block visibility options to block configuration form. + * + * @see language_form_block_admin_configure_submit() + * @see block_admin_configure() + */ +function language_form_block_admin_configure_alter(&$form, &$form_state) { + $default_langcode_options = db_query("SELECT langcode FROM {block_langcode} WHERE module = :module AND delta = :delta", array( + ':module' => $form['module']['#value'], + ':delta' => $form['delta']['#value'], + ))->fetchCol(); + + // Fetch the enabled languages. + $enabled_languages = language_list(TRUE); + foreach ($enabled_languages as $language) { + $langcodes_options[$language->langcode] = t($language->name); + } + $form['visibility']['langcode'] = array( + '#type' => 'fieldset', + '#title' => t('Languages'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'visibility', + '#weight' => 5, + ); + $form['visibility']['langcode']['langcodes'] = array( + '#type' => 'checkboxes', + '#title' => t('Show block for specific languages'), + '#default_value' => $default_langcode_options, + '#options' => $langcodes_options, + '#description' => t('Show this block only if the current language is of the given language(s). If you select no language, there will be no language-specific limitation.'), + ); + $form['#submit'][] = 'language_form_block_admin_configure_submit'; +} + +/** + * Form submission handler for locale_form_block_admin_configure_alter(). + */ +function language_form_block_admin_configure_submit($form, &$form_state) { + db_delete('block_langcode') + ->condition('module', $form_state['values']['module']) + ->condition('delta', $form_state['values']['delta']) + ->execute(); + $query = db_insert('block_langcode')->fields(array( + 'langcode', 'module', 'delta' + )); + foreach (array_filter($form_state['values']['langcodes']) as $langcode) { + $query->values(array( + 'langcode' => $langcode, + 'module' => $form_state['values']['module'], + 'delta' => $form_state['values']['delta'], + )); + } + $query->execute(); +} + +/** + * Implements hook_block_list_alter(). + * + * Hide the blocks that have been disabled for langcodes visibility setting. + */ +function language_block_list_alter(&$blocks) { + global $language_interface, $theme_key; + + $result = db_query('SELECT module, delta, langcode FROM {block_langcode}'); + $block_langcodes = array(); + foreach ($result as $record) { + $block_langcodes[$record->module][$record->delta][$record->langcode] = TRUE; + } + foreach ($blocks as $key => $block) { + // Any module using this alter should inspect the data before changing it, + // to ensure it is what they expect. + if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) { + // This block was added by a contrib module, leave it in the list. + continue; + } + + if (!isset($block_langcodes[$block->module][$block->delta])) { + // No language setting for this block, leave it in the list. + continue; + } + + if (!isset($block_langcodes[$block->module][$block->delta][$language_interface->langcode])) { + // This block should not be displayed with the active language, remove + // from the list. + unset($blocks[$key]); + } + } +} diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index a4982e1..3c5612a 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -34,62 +34,79 @@ function locale_translate_import_form($form, &$form_state) { else { $default = key($existing_languages); $language_options = array( - t('Already added languages') => $existing_languages, + t('Existing languages') => $existing_languages, t('Languages not yet added') => language_admin_predefined_list() ); } - $form['import'] = array('#type' => 'fieldset', - '#title' => t('Import translation'), - ); - $form['import']['file'] = array('#type' => 'file', - '#title' => t('Language file'), + $form['file'] = array( + '#type' => 'file', + '#title' => t('Translation file'), '#size' => 50, '#description' => t('A Gettext Portable Object (.po) file.'), ); - $form['import']['langcode'] = array('#type' => 'select', - '#title' => t('Import into'), + $form['langcode'] = array( + '#type' => 'select', + '#title' => t('Language'), '#options' => $language_options, '#default_value' => $default, - '#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.'), ); - $form['import']['mode'] = array('#type' => 'radios', - '#title' => t('Mode'), - '#default_value' => LOCALE_IMPORT_KEEP, - '#options' => array( - LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added. The plural format is updated.'), - LOCALE_IMPORT_KEEP => t('Existing strings and the plural format are kept, only new strings are added.') + $form['customized'] = array( + '#title' => t('Import contains custom translations'), + '#type' => 'checkbox', + ); + $form['overwrite_options'] = array( + '#type' => 'container', + '#tree' => TRUE, + ); + $form['overwrite_options']['not_customized'] = array( + '#title' => t('Overwrite existing non-customized translations'), + '#type' => 'checkbox', + '#states' => array( + 'checked' => array( + ':input[name=customized]' => array('checked' => TRUE), + ), ), ); - $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import')); + $form['overwrite_options']['customized'] = array( + '#title' => t('Overwrite existing customized translations'), + '#type' => 'checkbox', + ); + $form['actions'] = array( + '#type' => 'actions' + ); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Import') + ); return $form; } /** - * Process the locale import form submission. + * Processes the locale import form submission. */ function locale_translate_import_form_submit($form, &$form_state) { $validators = array('file_validate_extensions' => array('po')); - // Ensure we have the file uploaded + + // Ensure we have the file uploaded. if ($file = file_save_upload('file', $validators)) { - // Add language, if not yet supported - drupal_static_reset('language_list'); - $languages = language_list(); - $langcode = $form_state['values']['langcode']; - if (!isset($languages[$langcode])) { + // Add language, if not yet supported. + $language = language_load($form_state['values']['langcode']); + if (empty($language)) { include_once DRUPAL_ROOT . '/core/includes/standard.inc'; $predefined = standard_language_list(); $language = (object) array( - 'langcode' => $langcode, + 'langcode' => $form_state['values']['langcode'], ); - language_save($language); - drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0])))); + $language = language_save($language); + drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name)))); } + $customized = $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED; // Now import strings into the language - if ($return = _locale_import_po($file, $langcode, $form_state['values']['mode']) == FALSE) { + if ($return = _locale_import_po($file, $language->langcode, $form_state['values']['overwrite_options'], $customized) == FALSE) { $variables = array('%filename' => $file->filename); drupal_set_message(t('The translation import of %filename failed.', $variables), 'error'); watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR); @@ -106,11 +123,9 @@ function locale_translate_import_form_submit($form, &$form_state) { } /** - * User interface for the translation export screen. + * Builds form to export Gettext translation files. */ -function locale_translate_export_screen() { - // Get all enabled languages, except English, if we should not translate that. - drupal_static_reset('language_list'); +function locale_translate_export_form($form, &$form_state) { $languages = language_list(TRUE); $language_options = array(); foreach ($languages as $langcode => $language) { @@ -119,64 +134,55 @@ function locale_translate_export_screen() { } } - $output = ''; - // Offer translation export if any language is set up. - if (!empty($language_options)) { - $elements = drupal_get_form('locale_translate_export_po_form', $language_options); - $output = drupal_render($elements); - } - $elements = drupal_get_form('locale_translate_export_pot_form'); - $output .= drupal_render($elements); - return $output; -} - -/** - * Form to export PO files for the languages provided. - * - * @param $names - * An associate array with localized language names - */ -function locale_translate_export_po_form($form, &$form_state, $names) { - $form['export_title'] = array('#type' => 'item', - '#title' => t('Export translation'), + $form['langcode'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#options' => $language_options, + '#empty_option' => t('Source text only'), + '#empty_value' => LANGUAGE_SYSTEM, + ); + $form['content_options'] = array( + '#type' => 'container', + '#states' => array( + 'invisible' => array( + ':input[name=langcode]' => array('value' => LANGUAGE_SYSTEM), + ) + ), + '#tree' => TRUE, ); - $form['langcode'] = array('#type' => 'select', - '#title' => t('Language name'), - '#options' => $names, - '#description' => t('Select the language to export in Gettext Portable Object (.po) format.'), + $form['content_options']['customized'] = array( + '#type' => 'checkbox', + '#title' => t('Include customized translations'), + '#default_value' => TRUE, + ); + $form['content_options']['not_customized'] = array( + '#type' => 'checkbox', + '#title' => t('Include non-customized translations'), + '#default_value' => TRUE, + ); + $form['content_options']['not_translated'] = array( + '#type' => 'checkbox', + '#title' => t('Include untranslated text'), + '#default_value' => TRUE, ); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export')); - return $form; -} -/** - * Translation template export form. - */ -function locale_translate_export_pot_form() { - // Complete template export of the strings - $form['export_title'] = array('#type' => 'item', - '#title' => t('Export template'), - '#description' => t('Generate a Gettext Portable Object Template (.pot) file with all strings from the Drupal locale database.'), + $form['actions'] = array( + '#type' => 'actions' + ); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Export') ); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export')); - // Reuse PO export submission callback. - $form['#submit'][] = 'locale_translate_export_po_form_submit'; return $form; } /** - * Process a translation (or template) export form submission. + * Processes a translation (or template) export form submission. */ -function locale_translate_export_po_form_submit($form, &$form_state) { +function locale_translate_export_form_submit($form, &$form_state) { // If template is required, language code is not given. - $language = NULL; - if (isset($form_state['values']['langcode'])) { - $languages = language_list(); - $language = $languages[$form_state['values']['langcode']]; - } - _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language))); + $language = ($form_state['values']['langcode'] != LANGUAGE_SYSTEM ? language_load($form_state['values']['langcode']) : NULL); + _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $form_state['values']['content_options']))); } /** @@ -267,7 +273,7 @@ function locale_translate_batch_import($filepath, &$context) { // we can extract the language code to use for the import from the end. if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { $file = (object) array('filename' => drupal_basename($filepath), 'uri' => $filepath); - _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]); + _locale_import_read_po('db-store', $file, array(), $langcode[2]); $context['results'][] = $filepath; } } diff --git a/core/modules/locale/locale.css b/core/modules/locale/locale.css index 66de82b..96f08b9 100644 --- a/core/modules/locale/locale.css +++ b/core/modules/locale/locale.css @@ -6,7 +6,7 @@ #locale-translation-filter-form .form-item-language, #locale-translation-filter-form .form-item-translation, -#locale-translation-filter-form .form-item-group { +#locale-translation-filter-form .form-item-customized { float: left; /* LTR */ padding-right: .8em; /* LTR */ margin: 0.1em; diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index acf3a80..8ee3332 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -176,6 +176,12 @@ function locale_schema() { 'default' => '', 'description' => 'Language code. References {language}.langcode.', ), + 'customized' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, // LOCALE_NOT_CUSTOMIZED + 'description' => 'Boolean indicating whether the translation is modified.', + ), ), 'primary key' => array('language', 'lid'), 'foreign keys' => array( @@ -485,6 +491,27 @@ function locale_update_8005() { } /** + * Add column to track customized string status to locales_target. + */ +function locale_update_8006() { + $spec = array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, // LOCALE_NOT_CUSTOMIZED + ); + db_add_field('locales_target', 'customized', $spec); + + // If l10n_update module was installed retain its status data, but drop the + // old field from the table. + if (db_field_exists('locales_target', 'l10n_status')) { + db_update('locales_target') + ->expression('customized', 'l10n_status') + ->execute(); + db_drop_field('locales_target', 'l10n_status'); + } +} + +/** * @} End of "addtogroup updates-7.x-to-8.x" * The next series of updates should start at 9000. */ diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 6d3ded0..4b00e6c 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -124,7 +124,8 @@ function locale_menu() { ); $items['admin/config/regional/translate/export'] = array( 'title' => 'Export', - 'page callback' => 'locale_translate_export_screen', // possibly multiple forms concatenated + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translate_export_form'), 'access arguments' => array('translate interface'), 'weight' => 30, 'type' => MENU_LOCAL_TASK, diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 0d59f42..595d4e5 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -29,6 +29,7 @@ function _locale_translate_seek() { $query = array( 'translation' => 'all', 'language' => 'all', + 'customized' => 'all', 'string' => '', ); } @@ -36,13 +37,16 @@ function _locale_translate_seek() { $sql_query = db_select('locales_source', 's'); $sql_query->leftJoin('locales_target', 't', 't.lid = s.lid'); $sql_query->fields('s', array('source', 'location', 'context', 'lid')); - $sql_query->fields('t', array('translation', 'language')); + $sql_query->fields('t', array('translation', 'language', 'customized')); // Compute LIKE section. switch ($query['translation']) { case 'translated': $sql_query->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE'); $sql_query->orderBy('t.translation', 'DESC'); + if ($query['customized'] != 'all') { + $sql_query->condition('t.customized', $query['customized'], '='); + } break; case 'untranslated': $sql_query->condition(db_and() @@ -134,7 +138,7 @@ function _locale_translate_seek_query() { $query = &drupal_static(__FUNCTION__); if (!isset($query)) { $query = array(); - $fields = array('string', 'language', 'translation'); + $fields = array('string', 'language', 'translation', 'customized'); foreach ($fields as $field) { if (isset($_SESSION['locale_translation_filter'][$field])) { $query[$field] = $_SESSION['locale_translation_filter'][$field]; @@ -172,7 +176,25 @@ function locale_translation_filters() { $filters['translation'] = array( 'title' => t('Search in'), - 'options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')), + 'options' => array( + 'all' => t('Both translated and untranslated strings'), + 'translated' => t('Only translated strings'), + 'untranslated' => t('Untranslated strings') + ), + ); + + $filters['customized'] = array( + 'title' => t('Customized'), + 'options' => array( + 'all' => t('All'), + LOCALE_NOT_CUSTOMIZED => t('Non-customized only'), + LOCALE_CUSTOMIZED => t('Customized only'), + ), + 'states' => array( + 'visible' => array( + ':input[name=translation]' => array('value' => 'translated'), + ) + ), ); return $filters; @@ -210,6 +232,9 @@ function locale_translation_filter_form() { '#size' => 0, '#options' => $filter['options'], ); + if (isset($filter['states'])) { + $form['filters']['status'][$key]['#states'] = $filter['states']; + } } if (!empty($_SESSION['locale_translation_filter'][$key])) { $form['filters']['status'][$key]['#default_value'] = $_SESSION['locale_translation_filter'][$key]; @@ -435,21 +460,23 @@ function locale_translate_edit_form_submit($form, &$form_state) { } if ($has_translation) { // Only update or insert if we have a value to use. - if (!empty($translation)) { + if (!empty($translation) && ($translation != $value)) { db_update('locales_target') ->fields(array( 'translation' => $value, + 'customized' => LOCALE_CUSTOMIZED, )) ->condition('lid', $lid) ->condition('language', $langcode) ->execute(); } - else { + if (empty($translation)) { db_insert('locales_target') ->fields(array( 'lid' => $lid, 'translation' => $value, 'language' => $langcode, + 'customized' => LOCALE_CUSTOMIZED, )) ->execute(); } diff --git a/core/modules/locale/locale.test b/core/modules/locale/locale.test index f2395ad..0b43a7a 100644 --- a/core/modules/locale/locale.test +++ b/core/modules/locale/locale.test @@ -621,9 +621,11 @@ class LocalePluralFormatTest extends DrupalWebTestCase { // will not overwrite the proper plural formula imported above. $this->importPoFile($this->getPoFileWithMissingPlural(), array( 'langcode' => 'fr', + 'overwrite_options[not_customized]' => 1, )); $this->importPoFile($this->getPoFileWithBrokenPlural(), array( 'langcode' => 'hr', + 'overwrite_options[not_customized]' => 1, )); // Reset static caches from locale_get_plural() to ensure we get fresh data. @@ -809,7 +811,6 @@ class LocalePluralFormatTest extends DrupalWebTestCase { $name = tempnam('temporary://', "po_") . '.po'; file_put_contents($name, $contents); $options['files[file]'] = $name; - $options['mode'] = LOCALE_IMPORT_OVERWRITE; $this->drupalPost('admin/config/regional/translate/import', $options, t('Import')); drupal_unlink($name); } @@ -972,7 +973,6 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase { // strings are kept. $this->importPoFile($this->getOverwritePoFile(), array( 'langcode' => 'fr', - 'mode' => 1, // Existing strings are kept, only new strings are added. )); // The import should have created 1 string. @@ -994,7 +994,7 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase { // strings are overwritten. $this->importPoFile($this->getOverwritePoFile(), array( 'langcode' => 'fr', - 'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added. + 'overwrite_options[not_customized]' => 1, )); // The import should have updated 2 strings. @@ -1074,7 +1074,7 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase { // Try importing a .po file. $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array( 'langcode' => $langcode, - 'mode' => 0, + 'overwrite_options[not_customized]' => 1, )); $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.')); // This is the language indicator on the translation search screen for @@ -1316,9 +1318,6 @@ class LocaleExportFunctionalTest extends DrupalWebTestCase { */ function testExportTranslationTemplateFile() { // Get the translation template file. - // There are two 'Export' buttons on this page, but it somehow works. It'd - // be better if we could use the submit button id like documented but that - // doesn't work. $this->drupalPost('admin/config/regional/translate/export', array(), t('Export')); // Ensure we have a translation file. $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.')); diff --git a/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php b/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php index 99dc318..4ddf0f8 100644 --- a/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php +++ b/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php @@ -553,6 +553,39 @@ db_insert('locales_target')->fields(array( )) ->execute(); + // Add block_langcode table from language.install schema. + db_create_table('block_langcode', array( + 'fields' => array( + 'module' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'delta' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + ), + 'primary key' => array( + 'module', + 'delta', + 'langcode' + ), + 'indexes' => array( + 'langcode' => array( + 'langcode', + ), + ), + 'module' => 'language', + 'name' => 'block_langcode', + )); + // Set up variables needed for language support. db_insert('variable')->fields(array( 'name',