From 399d9a45204f9fc51096d2ec0a8400419fe64ade Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 14 Sep 2011 22:57:55 -0400 Subject: [PATCH] Issue #361597: Add Locale API and update Core to use it --- includes/gettext.inc | 110 ++++++--------- includes/locale.inc | 28 ++-- modules/locale/locale.module | 293 ++++++++++++++++++++++++++++++++++++--- modules/locale/locale.pages.inc | 63 ++++----- modules/locale/locale.test | 90 ++++++++++++ 5 files changed, 446 insertions(+), 138 deletions(-) diff --git a/includes/gettext.inc b/includes/gettext.inc index fa9952b..7cfb66e 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( '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 199edd1..3999584 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -544,34 +544,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('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 4a15ed9..bfb0157 100644 --- a/modules/locale/locale.module +++ b/modules/locale/locale.module @@ -659,12 +659,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); @@ -673,23 +675,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:'); @@ -794,9 +796,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') @@ -970,6 +970,257 @@ function locale_library_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 + * Source string. + */ +function locale_source_load($lid) { + $records = locale_source_load_multiple(array('lid' => $lid)); + return !empty($records) ? current($records) : FALSE; +} + +/** + * Get source for a string, given key/value conditions. + * + * @param $conditions + * An array of source lids, or an array of key => value pairs. + * @return + * Array of source strings (objects) that meet the condtions, indexed by lid. + */ +function locale_source_load_multiple(array $conditions) { + // Allow list of lids as arguments. + if (is_numeric(key($conditions))) { + $conditions = array('lid' => $conditions); + } + + // Build basic query on locales_source. + $query = db_select('locales_source', 's'); + $query->fields('s'); + foreach ($conditions as $field => $value) { + $query->condition($field, $value, is_array($value) ? 'IN' : '='); + } + return $query->execute()->fetchAllAssoc('lid'); +} + +/** + * Save / update a source string record. + * + * @param $source + * Object representing a source string. + * 'lid' property used for updates. + */ +function locale_source_save(&$source) { + // Ensure we're working with an object. + $source = (object)$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. + */ +function locale_source_delete($lid, $delete_translations = TRUE) { + locale_source_delete_multiple(array('lid' => $lid), $delete_translations); +} + + +/** + * Delete one or more locale source records from the database. Optionally, also + * delete associated target strings. + * + * @param $conditions + * An array of lids, or 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 $conditions, $delete_translations = TRUE) { + // Accept a single lid or an array of lids. + if (is_numeric(key($conditions))) { + $conditions = array('lid' => $conditions); + } + // If requested and deleting by ID, remove all associated targets. + if ($delete_translations) { + locale_translation_delete_multiple($conditions); + } + // Remove the sources. + $query = db_delete('locales_source'); + foreach ($conditions as $field => $value) { + $query->condition($field, $value, is_array($value) ? 'IN' : '='); + } + $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. + */ +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 !empty($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 + * The full translation object. + */ +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, is_array($value) ? 'IN' : '='); + } + + $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(&$translation) { + $translation = (object)$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 $translation + * Object representing a string translation. + */ +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 match. + * 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, is_array($value) ? 'IN' : '='); + } + else { + $subquery->condition($field, $value, is_array($value) ? 'IN' : '='); + } + } + // 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 3e6590e..fe05c88 100644 --- a/modules/locale/locale.pages.inc +++ b/modules/locale/locale.pages.inc @@ -297,7 +297,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'); @@ -342,9 +342,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'); @@ -372,34 +376,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. @@ -420,7 +423,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 { @@ -442,12 +445,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 90e313d..5607fcb 100644 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -591,6 +591,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->assertTrue($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->assertTrue($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->assertTrue(isset($id_loaded_source->lid) && $id_loaded_source->lid == $source->lid, t('Source loaded by ID')); + $this->assertTrue(isset($id_loaded_source->source) && $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->assertTrue(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->assertTrue($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->assertTrue($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->assertTrue(isset($id_loaded_target->lid) && $id_loaded_target->lid == $target->lid, t('Target loaded by ID')); + $this->assertTrue(isset($id_loaded_target->translation) && $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 { -- 1.7.5.3