diff --git a/core/includes/update.inc b/core/includes/update.inc index fe8034c..130fccd 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -412,6 +412,52 @@ function update_prepare_d8_language() { ); db_add_field('locales_target', 'customized', $spec); } + if (db_table_exists('locales_source') && db_field_exists('locales_source', 'location')) { + $table = array( + 'description' => 'Location information for source strings.', + 'fields' => array( + 'locid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Unique identifier of this location.', + ), + 'lid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'description' => 'Unique identifier of this string.', + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The location type (file, config, path, etc).', + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Drupal path in case of online discovered translations or file path in case of imported strings.', + ), + 'version' => array( + 'type' => 'varchar', + 'length' => 20, + 'not null' => TRUE, + 'default' => 'none', + 'description' => 'Version of Drupal, where the location was found (for locales optimization).', + ), + ), + 'primary key' => array('locid'), + 'indexes' => array( + 'location_type' => array('lid', 'type'), + 'type_name' => array('type', 'name'), + ), + ); + db_create_table('locales_location', $table); + // Drop old locales_source.location field. + db_drop_field('locales_source', 'location'); + } } } diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php index cd2f9c0..3c20467 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php @@ -70,9 +70,8 @@ protected function resolveCacheMiss($offset) { $this->stringStorage->createString(array( 'source' => $offset, 'context' => $this->context, - 'location' => request_uri(), 'version' => VERSION - ))->save(); + ))->addLocation('path', request_uri())->save(); $value = TRUE; } $this->storage[$offset] = $value; diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php index 7afa1db..f3de014 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -102,6 +102,7 @@ function setReport($report = array()) { 'updates' => 0, 'deletes' => 0, 'skips' => 0, + 'strings' => array(), ); $this->_report = $report; } @@ -226,7 +227,6 @@ private function importString(PoItem $item) { $source = $item->getSource(); $translation = $item->getTranslation(); - // Look up the source string and any existing translation. $string = locale_storage()->findTranslation(array( 'language' => $this->_langcode, @@ -258,6 +258,7 @@ private function importString(PoItem $item) { $string->save(); $this->_report['updates']++; } + $this->_report['strings'][] = $string->getId(); return $string->lid; } else { @@ -272,6 +273,7 @@ private function importString(PoItem $item) { ))->save(); $this->_report['additions']++; + $this->_report['strings'][] = $string->getId(); return $string->lid; } } @@ -279,6 +281,7 @@ private function importString(PoItem $item) { // Empty translation, remove existing if instructed. $string->delete(); $this->_report['deletes']++; + $this->_report['strings'][] = $string->lid; return $string->lid; } } diff --git a/core/modules/locale/lib/Drupal/locale/StringBase.php b/core/modules/locale/lib/Drupal/locale/StringBase.php index 8ff9815..80043d4 100644 --- a/core/modules/locale/lib/Drupal/locale/StringBase.php +++ b/core/modules/locale/lib/Drupal/locale/StringBase.php @@ -22,7 +22,7 @@ public $lid; /** - * The string location. + * The string locations indexed by type. * * @var string */ @@ -152,6 +152,35 @@ public function getValues(array $fields) { } /** + * Implements Drupal\locale\StringInterface::getLocation(). + */ + public function getLocation($load = TRUE) { + if ($load && !isset($this->location)) { + $this->location = array(); + foreach ($this->getStorage()->getLocations(array('lid' => $this->getId())) as $location) { + $this->location[$location->type][$location->name] = $location->locid; + } + } + return $this->location; + } + + /** + * Implements Drupal\locale\StringInterface::addLocation(). + */ + public function addLocation($type, $name) { + $this->location[$type][$name] = TRUE; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::hasLocation(). + */ + public function hasLocation($type, $name) { + $location = $this->getLocation(TRUE); + return isset($location[$type]) ? !empty($location[$type][$name]) : FALSE; + } + + /** * Implements Drupal\locale\LocaleString::save(). */ public function save() { diff --git a/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php index 38e9df5..7126aa9 100644 --- a/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php +++ b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php @@ -86,6 +86,21 @@ public function findTranslation(array $conditions) { } /** + * Gets strings locations. + * + * @return array + * Array of locations indexed by locale indentifier, type + */ + function getLocations(array $conditions) { + $query = $this->connection->select('locales_location', 'l', $this->options) + ->fields('l'); + foreach ($conditions as $field => $value) { + $query->condition('l.' . $field, $value); + } + return $query->execute()->fetchAll(); + } + + /** * Implements Drupal\locale\StringStorageInterface::countStrings(). */ public function countStrings() { @@ -127,10 +142,35 @@ public function save($string) { else { $this->dbStringUpdate($string); } + // Update locations if they come with the string. + $this->updateLocation($string); return $this; } /** + * Update locations for string. + */ + protected function updateLocation($string) { + if ($location = $string->getLocation(FALSE)) { + foreach ($location as $type => $type_location) { + foreach ($type_location as $name => $locid) { + if (!$locid) { + $this->dbDelete('locales_location', array('lid' => $string->getId(), 'type' => $type, 'name' => $name)); + } + elseif ($locid === TRUE) { + // This is a new location to add, take care not to duplicate. + $this->connection->merge('locales_location', $this->options) + ->key(array('lid' => $string->getId(), 'type' => $type, 'name' => $name)) + ->fields(array('version' => VERSION)) + ->execute(); + } + // Loaded locations have 'locid integer value, nor FALSE, nor TRUE. + } + } + } + } + + /** * Implements Drupal\locale\StringStorageInterface::delete(). */ public function delete($string) { @@ -138,6 +178,7 @@ public function delete($string) { $this->dbDelete('locales_target', $keys)->execute(); if ($string->isSource()) { $this->dbDelete('locales_source', $keys)->execute(); + $this->dbDelete('locales_location', $keys)->execute(); $string->setId(NULL); } } @@ -195,7 +236,15 @@ public function createTranslation($values = array()) { * target table. */ protected function dbFieldTable($field) { - return in_array($field, array('language', 'translation', 'customized')) ? 't' : 's'; + if (in_array($field, array('language', 'translation', 'customized'))) { + return 't'; + } + elseif (in_array($field, array('locid', 'type', 'name'))) { + return 'l'; + } + else { + return 's'; + } } /** @@ -367,6 +416,19 @@ protected function dbStringSelect(array $conditions, array $options = array()) { } } + // If we need the location table, we add a subquery. + if (isset($conditions['type']) || isset($conditions['name'])) { + $subquery = $this->connection->select('locales_location', 'l', $this->options) + ->fields('l', array('lid')); + foreach (array('type', 'name') as $field) { + if (isset($conditions[$field])) { + $subquery->condition('l.' . $field, $conditions[$field]); + unset($conditions[$field]); + } + } + $query->condition('s.lid', $subquery, 'IN'); + } + // Add conditions for both tables. foreach ($conditions as $field => $value) { $table_alias = $this->dbFieldTable($field); diff --git a/core/modules/locale/lib/Drupal/locale/StringInterface.php b/core/modules/locale/lib/Drupal/locale/StringInterface.php index ec04386..c51a020 100644 --- a/core/modules/locale/lib/Drupal/locale/StringInterface.php +++ b/core/modules/locale/lib/Drupal/locale/StringInterface.php @@ -158,6 +158,53 @@ public function setValues(array $values, $override = TRUE); public function getValues(array $fields); /** + * Gets location information for this string. + * + * Locations are arbitrary pairs of type and name strings, used to store + * information about the procedence of the string, like the file name it + * was found on, the path on which it was discovered, etc... + * + * A string can have any number of locations since the same string may be + * found on different places of Drupal code and configuration. + * + * @param bool $load + * (optional) Whether to load the string locations if not loaded yet. + * Defaults to TRUE. + * + * @return array + * Location ids indexed by type and name. + */ + public function getLocation($load = TRUE); + + /** + * Adds a location for this string. + * + * @param string $type + * Location type: 'javascript', 'path', 'configuration'... + * @param string $name + * Location name. Drupal path in case of online discovered translations, + * file path in case of imported strings, configuration name for strings + * that come from configuration, etc... + * + * @return Drupal\locale\LocaleString + * The called object. + */ + public function addLocation($type, $name); + + /** + * Checks whether the string has a given location. + * + * @param string $type. + * Location type. + * @param string $name. + * Location name. + * + * @return bool + * TRUE if the string has a location with this type and name. + */ + public function hasLocation($type, $name); + + /** * Saves string object to storage. * * @return Drupal\locale\LocaleString diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php index 8962267..1da06da 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php @@ -37,12 +37,9 @@ function testFileParsing() { _locale_parse_js_file($filename); // Get all of the source strings that were found. - $source_strings = db_select('locales_source', 's') - ->fields('s', array('source', 'context')) - ->condition('s.location', $filename) - ->execute() - ->fetchAllKeyed(); - + foreach (locale_storage()->getStrings(array('name' => $filename)) as $string) { + $source_strings[$string->source] = $string->context; + } // List of all strings that should be in the file. $test_strings = array( "Standard Call t" => '', diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUninstallTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUninstallTest.php index f3c528d..ec7dc42 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUninstallTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUninstallTest.php @@ -70,9 +70,9 @@ function testUninstallProcess() { $user = $this->drupalCreateUser(array('translate interface', 'access administration pages')); $this->drupalLogin($user); $this->drupalGet('admin/config/regional/translate/translate'); - $string = db_query('SELECT min(lid) AS lid, source FROM {locales_source} WHERE location LIKE :location', array( - ':location' => '%.js%', - ))->fetchObject(); + // Get any of the javascript strings to translate. + $js_strings = locale_storage()->getStrings(array('type' => 'javascript')); + $string = reset($js_strings); $edit = array('string' => $string->source); $this->drupalPost('admin/config/regional/translate', $edit, t('Filter')); $edit = array('strings[' . $string->lid . '][translations][0]' => 'french translation'); diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index b7850db..2fd98cd 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -492,17 +492,20 @@ function locale_translate_batch_import($filepath, $options, &$context) { $file->timestamp = filemtime($file->uri); locale_translate_update_file_history($file); $context['results']['files'][$filepath] = $filepath; + $context['results']['languages'][$filepath] = $file->langcode; } // Add the values from the report to the stats for this file. if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$filepath])) { $context['results']['stats'][$filepath] = array(); } foreach ($report as $key => $value) { - if (is_numeric($report[$key])) { - if (!isset($context['results']['stats'][$filepath][$key])) { - $context['results']['stats'][$filepath][$key] = 0; - } - $context['results']['stats'][$filepath][$key] += $report[$key]; + if (is_numeric($value)) { + $context['results']['stats'][$filepath] += array($key => 0); + $context['results']['stats'][$filepath][$key] += $value; + } + elseif (is_array($value)) { + $context['results']['stats'][$filepath] += array($key => array()); + $context['results']['stats'][$filepath][$key] = array_merge($context['results']['stats'][$filepath][$key], $value); } } } @@ -519,6 +522,7 @@ function locale_translate_batch_import($filepath, $options, &$context) { function locale_translate_batch_finished($success, $results) { if ($success) { $additions = $updates = $deletes = $skips = 0; + $strings = $langcodes = array(); drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.')); $skipped_files = array(); // If there are no results and/or no stats (eg. coping with an empty .po @@ -532,7 +536,11 @@ function locale_translate_batch_finished($success, $results) { if ($report['skips'] > 0) { $skipped_files[] = $filepath; } + $strings = array_merge($strings, $report['strings']); } + // Eliminage duplicates from string list and language codes. + $strings = array_unique($strings); + $langcodes = array_unique(array_values($results['languages'])); } 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))); watchdog('locale', 'The translation was succesfully imported. %number new strings added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); @@ -548,7 +556,7 @@ function locale_translate_batch_finished($success, $results) { } // Clear cache and force refresh of JavaScript translations. - _locale_invalidate_js(); + _locale_refresh_translations($langcodes, $strings); cache()->invalidateTags(array('locale' => TRUE)); } } diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index b470963..a38bba1 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -52,12 +52,6 @@ function locale_schema() { 'not null' => TRUE, 'description' => 'Unique identifier of this string.', ), - 'location' => array( - 'type' => 'text', - 'not null' => FALSE, - 'size' => 'big', - 'description' => 'Drupal path in case of online discovered translations or file path in case of imported strings.', - ), 'source' => array( 'type' => 'text', 'mysql_type' => 'blob', @@ -126,6 +120,48 @@ function locale_schema() { ), ); + $schema['locales_location'] = array( + 'description' => 'Location information for source strings.', + 'fields' => array( + 'locid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Unique identifier of this location.', + ), + 'lid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'description' => 'Unique identifier of this string.', + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 50, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The location type (file, config, path, etc).', + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Drupal path in case of online discovered translations or file path in case of imported strings.', + ), + 'version' => array( + 'type' => 'varchar', + 'length' => 20, + 'not null' => TRUE, + 'default' => 'none', + 'description' => 'Version of Drupal, where the location was found (for locales optimization).', + ), + ), + 'primary key' => array('locid'), + 'indexes' => array( + 'location_type' => array('lid', 'type'), + 'type_name' => array('type', 'name'), + ), + ); + $schema['locale_file'] = array( 'description' => 'File import status information for interface translation files.', 'fields' => array( @@ -741,6 +777,46 @@ function locale_update_8013() { } /** + * Add a locales_location table to replace locales_source location column. + */ +function locale_update_8015() { + //$table = drupal_get_schema('locales_location', TRUE); + //db_create_table('locales_location', $table); + // Copy only js file names which are the only ones that we are using atm. + /* + $result = db_select('locales_source', 's') + ->fields('s') + ->condition('s.location', NULL, 'IS NOT NULL'); + // Or this to update only js strings. + // ->condition('s.location', '%' . db_like('.js') . '%', 'LIKE'); + while ($string = $result->fetchObject()) { + $locations = $locations = preg_split('~\s*;\s*~', $string->location); + foreach ($locations as $location) { + $extension = array_pop(explode('.', $location)); + switch ($extension) { + case 'js': + $type = 'javascript'; + break; + case 'module': + case 'install': + case 'inc': + $type = 'code'; + break; + default: + // Guess based on firsth character, it should be '/' for paths + $type = strpos($location, '/') === 0 ? 'path' : 'code'; + break; + } + db_insert('locales_location') + ->fields(array('lid' => $string->lid, 'type' => 'javascript', 'name' => $location, 'version' => VERSION)); + } + } + */ + // Drop old locales_source.location field. + //db_drop_field('locales_source', 'location'); +} + +/** * @} 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 3d3ce1f..b39dec5 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -771,8 +771,35 @@ function locale_string_is_safe($string) { } /** + * Refresh related information after string translations have been updated. + * + * @param array $langcodes + * Language codes for updated translations. + * @param array $lids + * List of string identifiers that have been updated / created. + */ +function _locale_refresh_translations($langcodes, $lids) { + if ($lids && $langcodes) { + // Update javascript translations if any of the strings has a javascript location. + if ($strings = locale_storage()->getStrings(array('lid' => $lids, 'type' => 'javascript'))) { + array_map('_locale_invalidate_js', $langcodes); + } + // @todo Update translations for configuration strings too. + } +} + +/** * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and * Drupal.formatPlural() and inserts them into the database. + * + * Note this function must be called one final time with a FALSE argument to save + * pending updates. + * + * @param string $filepath + * File name to parse. + * + * @return array + * Array of string objects to update indexed by context and source. */ function _locale_parse_js_file($filepath) { // The file path might contain a query string, so make sure we only use the @@ -858,27 +885,20 @@ function _locale_parse_js_file($filepath) { $context = implode('', preg_split('~(?findString(array('source' => $string, 'context' => $context)); - 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. - $source->setValues(array('location' => $locations)) - ->save(); + if ($source) { + if (!$source->hasLocation('javascript', $filepath)) { + $source->addLocation('javascript', $filepath); + $source->save(); } } else { // We don't have the source string yet, thus we insert it into the database. - locale_storage()->createString(array( - 'location' => $filepath, + $source = locale_storage()->createString(array( 'source' => $string, 'context' => $context, - ))->save(); + ))->addLocation('javascript', $filepath) + ->save(); } } } @@ -934,10 +954,13 @@ function _locale_rebuild_js($langcode = NULL) { // Construct the array for JavaScript translations. // Only add strings with a translation to the translations array. - $options['filters']['location'] = '.js'; - $conditions['language'] = $language->langcode; + $conditions = array( + 'type' => 'javascript', + 'language' => $language->langcode, + 'translated' => TRUE, + ); $translations = array(); - foreach (locale_storage()->getTranslations($conditions, $options) as $data) { + foreach (locale_storage()->getTranslations($conditions) as $data) { $translations[$data->context][$data->source] = $data->translation; } diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 71aa14d..5d6bde1 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -324,11 +324,6 @@ function locale_translate_edit_form($form, &$form_state) { '#value' => check_plain($string->context), ); } - $form['strings'][$string->lid]['location'] = array( - '#type' => 'value', - '#value' => $string->location, - ); - // Approximate the number of rows to use in the default textarea. $rows = min(ceil(str_word_count($source_array[0]) / 12), 10); if (empty($form['strings'][$string->lid]['plural']['#value'])) { @@ -403,6 +398,7 @@ function locale_translate_edit_form_validate($form, &$form_state) { */ function locale_translate_edit_form_submit($form, &$form_state) { $langcode = $form_state['values']['langcode']; + $updated = array(); foreach ($form_state['values']['strings'] as $lid => $translations) { // Get target string, that may be NULL if there's no translation. $target = locale_storage()->findTranslation(array('language' => $langcode, 'lid' => $lid)); @@ -421,10 +417,12 @@ function locale_translate_edit_form_submit($form, &$form_state) { $target->setPlurals($translations['translations']) ->setCustomized() ->save(); + $updated[] = $target->getId(); } elseif ($target) { // Empty translation entered: remove existing entry from database. $target->delete(); + $updated[] = $target->getId(); } } @@ -436,7 +434,7 @@ function locale_translate_edit_form_submit($form, &$form_state) { } // Force JavaScript translation file recreation for this language. - _locale_invalidate_js($langcode); + _locale_refresh_translations(array($langcode), $updated); // Clear locale cache. cache()->invalidateTags(array('locale' => TRUE)); }