includes/gettext.inc | 110 +++++--------- includes/locale.inc | 28 ++-- modules/locale/locale.module | 308 ++++++++++++++++++++++++++++++++++++--- modules/locale/locale.pages.inc | 63 ++++---- modules/locale/locale.test | 90 ++++++++++++ 5 files changed, 461 insertions(+), 138 deletions(-) diff --git a/includes/gettext.inc b/includes/gettext.inc index a3d2a7c..688eb03 100644 --- a/includes/gettext.inc +++ b/includes/gettext.inc @@ -476,7 +476,17 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL * 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, $plid = 0, $plural = 0) { - $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $source, ':context' => $context))->fetchField(); + if ($sources = locale_source_load_multiple(array(), array('source' => $source, 'context' => $context))) { + $source = current($sources); + $source->location = $location; + } + else { + $source = (object) array( + 'location' => $location, + 'source' => $source, + 'context' => (string) $context, + ); + } if (!empty($translation)) { // Skip this string unless it passes a check for dangerous code. @@ -484,82 +494,44 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t $report['skips']++; $lid = 0; } - elseif ($lid) { - // We have this source string saved already. - db_update('locales_source') - ->fields(array( - 'location' => $location, - )) - ->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) { - // No translation in this language. - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'language' => $langcode, - 'translation' => $translation, - 'plid' => $plid, - 'plural' => $plural, - )) - ->execute(); - - $report['additions']++; + else { + $source_new = empty($source->lid); + locale_source_save($source); + + if (!$source_new && ($target = locale_translation_load($source->lid, $langcode, $plural))) { + $target->translation = $translation; + $target->plid = $plid; + if ($mode == LOCALE_IMPORT_OVERWRITE) { + // Translation exists, only overwrite if instructed. + locale_translation_save($target); + $report['updates']++; + } } - elseif ($mode == LOCALE_IMPORT_OVERWRITE) { - // Translation exists, only overwrite if instructed. - db_update('locales_target') - ->fields(array( - 'translation' => $translation, - 'plid' => $plid, - 'plural' => $plural, - )) - ->condition('language', $langcode) - ->condition('lid', $lid) - ->execute(); - - $report['updates']++; + else { + $target = (object) array( + 'lid' => $source->lid, + 'language' => $langcode, + 'translation' => $translation, + 'plid' => $plid, + 'plural' => $plural, + ); + locale_translation_save($target); + $report['additions']++; } } - else { - // No such source string in the database yet. - $lid = db_insert('locales_source') - ->fields(array( - 'location' => $location, - 'source' => $source, - 'context' => (string) $context, - )) - ->execute(); - - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'language' => $langcode, - 'translation' => $translation, - 'plid' => $plid, - 'plural' => $plural - )) - ->execute(); - - $report['additions']++; - } } - elseif ($mode == LOCALE_IMPORT_OVERWRITE) { + elseif (isset($source->lid) && $mode == LOCALE_IMPORT_OVERWRITE) { // Empty translation, remove existing if instructed. - db_delete('locales_target') - ->condition('language', $langcode) - ->condition('lid', $lid) - ->condition('plid', $plid) - ->condition('plural', $plural) - ->execute(); - + locale_translation_delete_multiple(array( + 'language' => $langcode, + 'lid' => $source->lid, + 'plid' => $plid, + 'plural' => $plural, + )); $report['deletes']++; } - return $lid; + return isset($source->lid) ? $source->lid : FALSE; } /** diff --git a/includes/locale.inc b/includes/locale.inc index 906a36e..2836040 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -598,34 +598,30 @@ function _locale_parse_js_file($filepath) { // Remove the quotes and string concatenations from the string. $string = implode('', preg_split('~(? $string))->fetchObject(); - if ($source) { + $sources = locale_source_load_multiple(array(), array('source' => $string)); + + if ($sources) { + $source = current($sources); // 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); + $source->location = implode('; ', $locations); // Save the new locations string to the database. - db_update('locales_source') - ->fields(array( - 'location' => $locations, - )) - ->condition('lid', $source->lid) - ->execute(); + locale_source_save($source); } } 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' => '', - )) - ->execute(); + $source = (object) array( + 'location' => $filepath, + 'source' => $string, + 'context' => '', + ); + locale_source_save($source); } } } diff --git a/modules/locale/locale.module b/modules/locale/locale.module index 2037c35..628478d 100644 --- a/modules/locale/locale.module +++ b/modules/locale/locale.module @@ -663,12 +663,14 @@ function locale($string = NULL, $context = NULL, $langcode = NULL) { 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) { + $translations = locale_translation_load_multiple(array( + 'language' => $langcode, + 'source' => $string, + 'context' => (string) $context, + ), TRUE); // includes source data in translation objects. + + if (!empty($translations)) { + $translation = current($translations); // 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); @@ -677,23 +679,23 @@ function locale($string = NULL, $context = NULL, $langcode = NULL) { // 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(); + $translation->version = VERSION; + + // translation object includes source data here. + locale_source_save($translation); + cache()->deletePrefix('locale:'); } } else { // We don't have the source string, cache this as untranslated. - db_insert('locales_source') - ->fields(array( - 'location' => request_uri(), - 'source' => $string, - 'context' => (string) $context, - 'version' => VERSION, - )) - ->execute(); + $source = (object) array( + 'location' => request_uri(), + 'source' => $string, + 'context' => (string) $context, + 'version' => VERSION, + ); + locale_source_save($source); $locale_t[$langcode][$context][$string] = TRUE; // Clear locale cache so this string can be added in a later request. cache()->deletePrefix('locale:'); @@ -798,9 +800,7 @@ function locale_language_delete($langcode) { module_invoke_all('locale_language_delete', $language); // Remove translations first. - db_delete('locales_target') - ->condition('language', $language->language) - ->execute(); + locale_translation_delete_multiple(array('language' => $language->language)); // Remove the language. db_delete('languages') @@ -974,6 +974,272 @@ function locale_library_info_alter(&$libraries, $module) { } } +// -------------------------------------------------------------------------------- +// Locale CRUD API + +/** + * @defgroup locale-api Locale CRUD API + * @{ + * API for managing Locale sources and targets + */ + +/** + * Load a source string record. + * + * @param $lid + * A source lid. + * + * @return + * A source string object with the given lid, or FALSE. + */ +function locale_source_load($lid) { + $records = locale_source_load_multiple(array($lid)); + return $records ? current($records) : FALSE; +} + +/** + * Get source for a string, given key/value conditions. + * + * @param $lids + * An array of source lids to load. + * @param $conditions + * An array of key => value condition pairs. + * + * @return + * Array of source string objects, indexed by lid. + */ +function locale_source_load_multiple(array $lids, array $conditions = array()) { + + // Build basic query on locales_source. + $query = db_select('locales_source', 's'); + $query->fields('s'); + if ($lids) { + $query->condition('lid', $lids); + } + foreach ($conditions as $field => $value) { + $query->condition($field, $value); + } + return $query->execute()->fetchAllAssoc('lid'); +} + +/** + * Save / update a source string record. + * + * @param $source + * Object representing a source string. + * 'lid' property used for updates. + * + * @return + * SAVED_NEW or SAVED_UPDATED. + */ +function locale_source_save(stdClass $source) { + if (!empty($source->lid)) { + return drupal_write_record('locales_source', $source, 'lid'); + } + else { + return drupal_write_record('locales_source', $source); + } +} + +/** + * Delete a specific source record from the database. + * + * @param $lid + * The lid of source string to delete. + * @param $delete_translations + * Flag indicating whether the translations for this source should be removed + * along with the source string. + */ +function locale_source_delete($lid, $delete_translations = TRUE) { + locale_source_delete_multiple(array($lid), array(), $delete_translations); +} + + +/** + * Delete one or more locale source records from the database. + * + * Optionally, also delete associated target strings. + * + * @param $lids + * An array of lids. + * @param $conditions + * An array field-value pairs to match. Values may be arrays, in which case + * matching will use the 'IN' operator. + * @param $delete_translations + * Boolean, whether to delete all associated target strings. + */ +function locale_source_delete_multiple(array $lids, array $conditions = array(), $delete_translations = TRUE) { + + // If requested and deleting by ID, remove all associated targets. + if ($delete_translations) { + locale_translation_delete_multiple($conditions + array('lid' => $lids)); + } + // Remove the sources. + $query = db_delete('locales_source'); + if ($lids) { + $query->condition('lid', $lids); + } + foreach ($conditions as $field => $value) { + $query->condition($field, $value); + } + $query->execute(); +} + +/** + * Load a translation string record. + * + * @param $lid + * The source id of the translation. + * @param $langcode + * The language code of the translation. + * @param $plural + * The plural of the translation. + * @param $get_source + * Whether to get source data only in case there's no translation. + * + * @return + * The first translation object matching the given conditions, or FALSE. + */ +function locale_translation_load($lid, $langcode, $plural = 0, $get_source = FALSE) { + $records = locale_translation_load_multiple(array('lid' => $lid, 'language' => $langcode, 'plural' => $plural), $get_source); + return $records ? current($records) : FALSE; +} + +/** + * Load one or more translations. + * + * @param $conditions + * An array of source lids or an array of key => value pairs. + * @param $get_source + * Whether to get source data only in case there's no translation. + * + * @return + * An array of translation object matching the given conditions. + */ +function locale_translation_load_multiple(array $conditions, $get_source = FALSE) { + // Get target fields to check which table each condition is in. + $target_schema = drupal_get_schema('locales_target'); + $target_field_info = $target_schema['fields']; + + // Check for an array of lids. + if (is_numeric(key($conditions))) { + $conditions = array('lid' => $conditions); + } + + $query = db_select('locales_source', 's'); + // If we want source data too, add fields and LEFT JOIN, otherwise no fields + // and INNER JOIN. + if ($get_source) { + $query->fields('s'); + $join = 'leftJoin'; + + // exclude lid because that is ALSO in locales_source + unset($target_field_info['lid']); + } + else { + $join = 'join'; + } + $target_fields = array_keys($target_field_info); + + // Join localse_target table, and add all fields except 'lid'. + $query->$join('locales_target', 't', 's.lid = t.lid'); + + // Not using '*' because then target lid overrides source lid. + $query->fields('t', $target_fields); + + foreach ($conditions as $field => $value) { + $table_alias = in_array($field, $target_fields) ? 't' : 's'; + $query->condition($table_alias . '.' . $field, $value); + } + + $translations = array(); + foreach ($query->execute() as $row) { + // Make uniqe identifier by appending primary-key items. + $translations[$row->lid . '-' . $row->language . '-' . $row->plural] = $row; + } + return $translations; +} + +/** + * Save a translation. + * + * @param $translation + * Object representing a string translation. + * + * @return + * SAVED_NEW or SAVED_UPDATED. + */ +function locale_translation_save(stdClass $translation) { + + // Check again for existing source and translation. + $existing = isset($translation->plural) && locale_translation_load($translation->lid, $translation->language, $translation->plural); + if (!empty($existing)) { + return drupal_write_record('locales_target', $translation, array('lid', 'language', 'plural')); + } + else { + return drupal_write_record('locales_target', $translation); + } +} + +/** + * Delete one locale target record from database. + * + * @param $lid + * The lid of the translation to remove. + * @param $langcode + * The language code of the translation to remove. + * @param $plural + * The plural id of the translation to remove. + */ +function locale_translation_delete($lid, $langcode, $plural = 0) { + locale_translation_delete_multiple(array( + 'lid' => $lid, + 'language' => $langcode, + 'plural' => $plural, + )); +} + +/** + * Delete one or more locale target records from the database. + * + * @param $conditions + * An array of lids, or an array field-value pairs to query for deletion. + * Values may be arrays, in which case matching will use the 'IN' operator. + */ +function locale_translation_delete_multiple(array $conditions) { + $target_schema = drupal_get_schema('locales_target'); + $target_fields = array_keys($target_schema['fields']); + + // Accept an array of lids. + if (is_numeric(key($conditions))) { + $conditions = array('lid' => $conditions); + } + + // Build the query which may have conditions for the source table and the + // target table. + $query = db_delete('locales_target'); + $subquery = db_select('locales_source', 's'); + foreach ($conditions as $field => $value) { + if (in_array($field, $target_fields)) { + $query->condition($field, $value); + } + else { + $subquery->condition($field, $value); + } + } + // If we have conditions for the target table we need to build a IN subquery. + if ($subquery->conditions()) { + $subquery->addField('s', 'lid'); + $query->condition('lid', $subquery, 'IN'); + } + + $query->execute(); +} + +/** + * @} End of "locale-api" + */ + // --------------------------------------------------------------------------------- // Language switcher block diff --git a/modules/locale/locale.pages.inc b/modules/locale/locale.pages.inc index 26de396..6e9bde2 100644 --- a/modules/locale/locale.pages.inc +++ b/modules/locale/locale.pages.inc @@ -266,7 +266,7 @@ function locale_translation_filter_form_submit($form, &$form_state) { */ function locale_translate_edit_form($form, &$form_state, $lid) { // Fetch source string, if possible. - $source = db_query('SELECT source, context, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject(); + $source = locale_source_load($lid); if (!$source) { drupal_set_message(t('String not found.'), 'error'); drupal_goto('admin/config/regional/translate/translate'); @@ -311,9 +311,13 @@ function locale_translate_edit_form($form, &$form_state, $lid) { } // Fetch translations and fill in default values in the form. - $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid", array(':lid' => $lid)); - foreach ($result as $translation) { - $form['translations'][$translation->language]['#default_value'] = $translation->translation; + $translations = locale_translation_load_multiple(array('lid' => $lid)); + $plurals = array(); + foreach ($translations as $translation) { + if (!isset($plurals[$translation->language][$translation->plural])) { + $form['translations'][$translation->language]['#default_value'] = $translation->translation; + } + $plurals[$translation->language][$translation->plural] = TRUE; } $form['actions'] = array('#type' => 'actions'); @@ -341,34 +345,33 @@ function locale_translate_edit_form_validate($form, &$form_state) { function locale_translate_edit_form_submit($form, &$form_state) { $lid = $form_state['values']['lid']; foreach ($form_state['values']['translations'] as $key => $value) { - $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField(); + $translations = locale_translation_load_multiple(array( + 'lid' => $lid, + 'language' => $key, + )); if (!empty($value)) { // Only update or insert if we have a value to use. - if (!empty($translation)) { - db_update('locales_target') - ->fields(array( - 'translation' => $value, - )) - ->condition('lid', $lid) - ->condition('language', $key) - ->execute(); + if (!empty($translations)) { + foreach ($translations as $translation) { + $translation->translation = $value; + locale_translation_save($translation); + } } else { - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'translation' => $value, - 'language' => $key, - )) - ->execute(); + $translation = (object)array( + 'lid' => $lid, + 'translation' => $value, + 'language' => $key, + ); + locale_translation_save($translation); } } - elseif (!empty($translation)) { + elseif (!empty($translations)) { // Empty translation entered: remove existing entry from database. - db_delete('locales_target') - ->condition('lid', $lid) - ->condition('language', $key) - ->execute(); + locale_translation_delete_multiple(array( + 'lid' => $lid, + 'langauge' => $key, + )); } // Force JavaScript translation file recreation for this language. @@ -389,7 +392,7 @@ function locale_translate_edit_form_submit($form, &$form_state) { * String deletion confirmation page. */ function locale_translate_delete_page($lid) { - if ($source = db_query('SELECT lid, source FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject()) { + if ($source = locale_source_load($lid)) { return drupal_get_form('locale_translate_delete_form', $source); } else { @@ -411,12 +414,8 @@ function locale_translate_delete_form($form, &$form_state, $source) { * Process string deletion submissions. */ function locale_translate_delete_form_submit($form, &$form_state) { - db_delete('locales_source') - ->condition('lid', $form_state['values']['lid']) - ->execute(); - db_delete('locales_target') - ->condition('lid', $form_state['values']['lid']) - ->execute(); + locale_source_delete($form_state['values']['lid']); + // Force JavaScript translation file recreation for all languages. _locale_invalidate_js(); cache()->deletePrefix('locale:'); diff --git a/modules/locale/locale.test b/modules/locale/locale.test index b94f565..acd7aa2 100644 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -642,6 +642,96 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase { } /** + * Tests for locale CRUD API functions. + */ +class LocaleApiTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => t('Locale API functions'), + 'description' => t('Tests the performance of locale CRUD APIs.'), + 'group' => t('Locale'), + ); + } + + function setUp() { + parent::setUp('locale', 'locale_test'); + include_once DRUPAL_ROOT . '/includes/locale.inc'; + // Create and login user. + $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer languages', 'translate interface', 'access administration pages')); + $this->drupalLogin($admin_user); + } + + /** + * Test locale source APIs. + */ + function testApis() { + // Insert an initial source string. + $source = (object)array( + 'location' => 'test', + 'source' => 'new', + 'context' => 'context', + 'version' => 'none', + ); + $insert_result = locale_source_save($source); + $this->assertEqual($insert_result, SAVED_NEW, t('Correct value returned when new locale source string saved')); + + // Update the initial source string after changing a property. + $source->source = 'changed'; + $update_result = locale_source_save($source); + $this->assertEqual($update_result, SAVED_UPDATED, t('Correct value returned when new locale source string updated')); + + // Load the source string. + $id_loaded_source = locale_source_load($source->lid); + $this->assertEqual($id_loaded_source->lid, $source->lid, t('Source loaded by ID')); + $this->assertEqual($id_loaded_source->source, 'changed', t('Source updated')); + + // Save a second source string. + $lids = array($source->lid); + unset($source->lid); + locale_source_save($source); + $lids[] = $source->lid; + // Load multiple strings. + $multiple = locale_source_load_multiple($lids); + $this->assertEqual(count($multiple), 2, t('Multiple locale source strings loaded by ID')); + + // Add language. + $edit = array( + 'langcode' => 'fr', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Create a locale target string. + $target = (object)array( + 'lid' => current($lids), + 'translation' => 'analyse', + 'language' => 'fr', + 'plural' => 0, + ); + $insert_result = locale_translation_save($target); + $this->assertEqual($insert_result, SAVED_NEW, t('Correct value returned when new locale target string saved')); + + // Update the initial target string. + $target->translation = 'tester'; + $update_result = locale_translation_save($target); + $this->assertEqual($update_result, SAVED_UPDATED, t('Correct value returned when new locale target string updated')); + + // Load the target string. + $id_loaded_target = locale_translation_load($target->lid, $target->language); + $this->assertEqual($id_loaded_target->lid, $target->lid, t('Target loaded by ID')); + $this->assertEqual($id_loaded_target->translation, 'tester', t('Target updated')); + + // Delete source and target strings. + locale_source_delete($lids); + $sources = locale_source_load_multiple($lids); + $this->assertTrue(empty($sources), t('Locale source strings deleted')); + + $translations = locale_translation_load_multiple($lids); + $this->assertTrue(empty($translations), t('Locale target strings deleted')); + } +} + + +/** * Functional tests for the import of translation files. */ class LocaleImportFunctionalTest extends DrupalWebTestCase {