diff --git a/core/includes/update.inc b/core/includes/update.inc index fe8034c..c1c921e 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -412,6 +412,82 @@ 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); + // 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'); + } } } diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php index 30d12ec..7dcf1c1 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php @@ -74,9 +74,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/StringBase.php b/core/modules/locale/lib/Drupal/locale/StringBase.php index fdb35e8..babb8f4 100644 --- a/core/modules/locale/lib/Drupal/locale/StringBase.php +++ b/core/modules/locale/lib/Drupal/locale/StringBase.php @@ -19,7 +19,7 @@ public $lid; /** - * The string location. + * The string locations indexed by type. * * @var string */ @@ -142,6 +142,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 01927c7..1087d2c 100644 --- a/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php +++ b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php @@ -90,6 +90,21 @@ public function findTranslation(array $conditions, array $options = array()) { } /** + * 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() { @@ -131,10 +146,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) { @@ -199,7 +239,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'; + } } /** @@ -332,6 +380,17 @@ protected function dbStringLoad(array $conditions, array $options, $class) { * Query object with all the tables, fields and conditions. */ protected function dbStringSelect(array $conditions, array $options = array()) { + // Clasify conditions per table and extract filters. + $table_conditions = $filters = array(); + foreach ($conditions as $field => $value) { + if (strpos($field, '%') !== FALSE) { + $field = trim($field, '%'); + $filters[$this->dbFieldTable($field)][$field] = $value; + } + else { + $table_conditions[$this->dbFieldTable($field)][] = $field; + } + } // Check the fields we are going to select and to which table they belong. $fields = array(); if (isset($options['fields'])) { @@ -384,6 +443,20 @@ protected function dbStringSelect(array $conditions, array $options = array()) { $query->$join('locales_target', 't', "t.lid = s.lid"); } } + + // 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 fields for both tables, it may be a query without fields. foreach ($fields as $table_alias => $table_fields) { $query->fields($table_alias, $table_fields); diff --git a/core/modules/locale/lib/Drupal/locale/StringInterface.php b/core/modules/locale/lib/Drupal/locale/StringInterface.php index c39ac70..8edb222 100644 --- a/core/modules/locale/lib/Drupal/locale/StringInterface.php +++ b/core/modules/locale/lib/Drupal/locale/StringInterface.php @@ -150,6 +150,67 @@ 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) { + 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; + } + + /** + * 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) { + $this->location[$type][$name] = TRUE; + return $this; + } + + /** + * 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) { + $location = $this->getLocation(TRUE); + return isset($location[$type]) ? !empty($location[$type][$name]) : FALSE; + } + + /** * 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 9f63420..8ce9930 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.config.inc b/core/modules/locale/locale.config.inc new file mode 100644 index 0000000..fa70c11 --- /dev/null +++ b/core/modules/locale/locale.config.inc @@ -0,0 +1,134 @@ +getLocations(array('lid' => $lids, 'type' => 'configuration')); + $name_strings = array(); + foreach ($locations as $location) { + $name_strings[$locations->name][] = $location->lid; + } + // Update configuration translation data. + if ($name_strings) { + locale_config_update_multiple(array_keys($name_strings), array($langcode)); + } +} + +/** + * Create translated configuration for module, all languages. + * + * This will run: + * - When the module is installed. + * - When a language is added (then we translate config for all installed modules) + */ +function locale_config_import_module($module) { + $directory = drupal_get_path('module', $module); + $storage = new FileStorage($directory); + if ($names = $storage->listAll()) { + $langcodes = array_keys(language_list()); + return locale_config_update_multiple($names, $langcodes); + } +} + +/** + * Enable language, thus update all modules for that language. + */ +function locale_config_import_language($langcode) { + $names = array_keys(locale_config_default_folder()); + return locale_config_update_multiple($names, array($langcode)); +} + +/** + * Update all configuration for names / languages. + */ +function locale_config_update_multiple($names, $langcodes) { + $count = 0; + foreach ($names as $name) { + if ($data = locale_config_default_data($name)) { + $wrapper = new ConfigWrapper($name, $data); + foreach ($langcodes as $lang) { + $translation = $wrapper->getTranslation($lang, TRUE); + locale_config_update_storage($name, $langcode, $translation); + $count++; + } + } + } + return $count; +} + +/** + * Get a default configuration wrapper for config name. + * + * @todo The config objects are useless for this, ask some changes from the guys. + * @todo Format the previous @todo to 80 chars ;-) + */ +function locale_config_update_storage($name, $langcode, $data) { + $locale_name = 'locale.config.' . $langcode . '.' . $name; + $storage = drupal_container()->get('config.storage'); + // If there are existing translations for whatever reason we don't want to lose them + $current = $storage->read($locale_name); + $updated = $current ? NestedArray::mergeDeep($current, $data) : $data; + if ($updated) { + $storage->write($locale_name, $updated); + } + else { + $storage->delete($locale_name); + } + return $updated; +} + +/** + * Get default data for configuration name. + */ +function locale_config_default_data($name) { + if ($directory = locale_config_default_folder($name)) { + $storage = new FileStorage($directory); + return $storage->read($name); + } + else { + return array(); + } +} + +/** + * Get all default configuration names and the path on which they are. + * + * @todo Do the same for themes? + */ +function locale_config_default_folder($name = NULL) { + $folders = &drupal_static(__FUNCTION__); + if (!$folders) { + foreach (module_list() as $module) { + $directory = drupal_get_path('module', $module) . '/config'; + if (file_exists($directory)) { + $storage = new FileStorage($directory); + foreach ($storage->listAll() as $config_name) { + $folders[$config_name] = $directory; + } + } + } + } + if ($name) { + return isset($folders[$name]) ? $folders[$name] : NULL; + } + else { + return $folders; + } +} + diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index 30dd788..5b54fde 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 eba008c..8ef90f9 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -343,6 +343,40 @@ function locale($string = NULL, $context = NULL, $langcode = NULL) { } /** + * Translate configuration string. + * + * This is a different version of locale() for configuration strings that: + * - Checks the strings has the right 'configuration' location. + * - Checks the stored source string matches the right configuration value. + * - Doesn't return a translation, but FALSE instead, if there's no translation. + * + * @param string $name + * Configuration name where the string is found. + * @param string $string + * Configuration string that is suppossed to be a default value. + * + * @param boolean $update + * (optional) Whether to update the source string. Defaults to FALSE. + */ +function locale_config($name, $string, $context, $langcode, $update = FALSE) { + $string = locale_storage()->findTranslation(array('source' => $source, 'context' => $context, 'language' => $langcode)); + if ($string) { + if ($update && !$string->hasLocation('configuration', $name)) { + $string->addLocation('configuration', $name); + $string->save(); + } + return $string->getString(); + } + elseif($update) { + // Create source and tag for this config name. + $string = locale_storage()->createSource(array('source' => $string, 'context' => $context)) + ->addLocation('configuration', $name) + ->save(); + } + return FALSE; +} + +/** * Reset static variables used by locale(). */ function locale_reset() { @@ -773,6 +807,15 @@ function locale_string_is_safe($string) { /** * 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 +901,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,9 +970,10 @@ 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'; + // $options['filters']['location'] = '.js'; $options['fields'] = array('lid', 'context', 'source', 'translation'); $conditions['language'] = $language->langcode; + $conditions['type'] = 'javascript'; $translations = array(); foreach (locale_storage()->getTranslations($conditions, $options) 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 bfb5796..7b1e8f6 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -324,11 +324,12 @@ 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'])) {