Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.864 diff -u -p -r1.864 common.inc --- includes/common.inc 5 Feb 2009 01:21:16 -0000 1.864 +++ includes/common.inc 5 Feb 2009 07:41:24 -0000 @@ -3952,7 +3952,9 @@ function drupal_write_record($table, &$o $object->$serial = $last_insert_id; } } - else { + // If we have a single-field primary key but got no insert ID, the + // query failed. + elseif (count($primary_keys) == 1) { $return = FALSE; } @@ -3965,6 +3967,139 @@ function drupal_write_record($table, &$o } /** + * Load a record from the database, consulting the schema if necessary. + * + * @param $table + * The name of the table; this must exist in schema API. + * @param $alias + * An alias for the table. + * @param $conditions + * An ID or array of conditions to apply to the query. + * @param $fields + * Array: the names of the fields to be returned. If empty, all fields will be + * returned. + * @param $alter + * Boolean, whether to pass the results through drupal_alter(). + * @return + * An array of all matching records, or false on failure. + */ +function drupal_load_record($table, $alias = NULL, $conditions = array(), $fields = NULL, $alter = FALSE, $unserialize = FALSE) { + $records = drupal_load_records($table, $alias, $conditions, $fields, $alter, $unserialize); + if (!empty($records)) { + return $records[0]; + } + else { + return FALSE; + } +} + +/** + * Load one or more records from the database, consulting the schema if necessary. + * + * @param $table + * The name of the table; this must exist in schema API. + * @param $alias + * An alias for the table. + * @param $conditions + * An array of conditions to apply to the query. + * @param $fields + * Array: the names of the fields to be returned. If empty, all fields will be + * returned. + * @param $alter + * Boolean, whether to pass the results through drupal_alter(). + * @return + * An array of all matching records, or false on failure. + */ +function drupal_load_records($table, $alias = NULL, $conditions = array(), $fields = array(), $alter = FALSE, $unserialize = FALSE) { + + // If no alias was specified, use the full table name. + if (empty($alias)) { + $alias = $table; + } + + // We need schema information if loading by numerical ID or if unserializing + // unserializing without a list of fields to unserialize. + if ((!empty($unserialize) && !is_array($unserialize)) || is_numeric($conditions) || is_numeric(key($conditions))) { + $schema = drupal_get_schema($table); + if (empty($schema)) { + return FALSE; + } + // Accept a numeric ID key or an array of IDs. + if (is_numeric($conditions) || is_numeric(key($conditions))) { + $primary_keys = $schema['primary key']; + if (count($primary_keys) > 1) { + return FALSE; + } + $conditions = array($primary_keys[0], $conditions); + } + } + $query = db_select($table, $alias); + $query->fields($alias, $fields); + foreach ($conditions as $field => $value) { + $query->condition($field, $value, is_array($value) ? 'IN' : '='); + } + + $result = $query->execute()->fetchAll(); + if (empty($result)) { + return FALSE; + } + foreach (array_keys($result) as $key) { + // Send the results for altering if requested. + if ($alter) { + drupal_alter('drupal_read_records', $result[$key], $table); + } + if (!empty($unserialize)) { + // Iterate through result records. + foreach ($result[$key] as $field => $value) { + // If required, unserialize results. + if (in_array($field, $unserialize) || ($unserialize == TRUE && isset($schema['fields'][$field]) && !empty($schema['fields'][$field]['serialize']))) { + $result[$key]->$field = unserialize($value); + } + } + } + } + + return $result; +} + +/** + * Delete one or more records from the database, consulting the schema if + * necessary. + * + * @param $table + * The name of the table. + * @param $conditions + * The conditions to match for deletion. If an integer or array of integers is + * given, these are treated as primary key values with the primary key being + * determined from the schema. Matching criteria may also be fed as an array + * of key-value pairs keyed by field name, in which case the schema is not + * consulted. + * + * @return + * Failure to delete based on missing schema information will return FALSE. + * Otherwise SAVED_DELETED. + */ +function drupal_delete_records($table, $conditions) { + if (is_numeric($conditions) || is_numeric(key($conditions))) { + $schema = drupal_get_schema($table); + if (empty($schema)) { + return FALSE; + } + if (count($schema['primary keys']) > 1) { + return FALSE; + } + $primary_key = current($schema['primary keys']); + $conditions = array($primary_key => $value); + } + $query = db_delete($table); + foreach ($conditions as $field => $value) { + $query->condition($field, $value, is_array($value) ? 'IN' : '='); + } + + $query->execute(); +} + +/** * @} End of "ingroup schemaapi". */ Index: includes/locale.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/locale.inc,v retrieving revision 1.202 diff -u -p -r1.202 locale.inc --- includes/locale.inc 5 Feb 2009 00:32:46 -0000 1.202 +++ includes/locale.inc 5 Feb 2009 07:41:34 -0000 @@ -855,7 +855,7 @@ function locale_translate_export_po_form */ function locale_translate_edit_form(&$form_state, $lid) { // Fetch source string, if possible. - $source = db_fetch_object(db_query('SELECT source, textgroup, location FROM {locales_source} WHERE lid = %d', $lid)); + $source = locale_source_load($lid); if (!$source) { drupal_set_message(t('String not found.'), 'error'); drupal_goto('admin/build/translate/translate'); @@ -951,19 +951,17 @@ function locale_translate_edit_form_vali function locale_translate_edit_form_submit($form, &$form_state) { $lid = $form_state['values']['lid']; foreach ($form_state['values']['translations'] as $key => $value) { - $translation = db_result(db_query("SELECT translation FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $key)); + // Only update or insert if we have a value to use. if (!empty($value)) { - // Only update or insert if we have a value to use. - if (!empty($translation)) { - db_query("UPDATE {locales_target} SET translation = '%s' WHERE lid = %d AND language = '%s'", $value, $lid, $key); - } - else { - db_query("INSERT INTO {locales_target} (lid, translation, language) VALUES (%d, '%s', '%s')", $lid, $value, $key); - } + $translation = new StdClass(); + $translation->lid = $lid; + $translation->translation = $value; + $tranlation->language = $key; + locale_target_save($translation); } - elseif (!empty($translation)) { + else { // Empty translation entered: remove existing entry from database. - db_query("DELETE FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $key); + locale_target_delete($lid, $key); } // Force JavaScript translation file recreation for this language. @@ -2735,3 +2733,276 @@ function _locale_batch_language_finished /** * @} End of "locale-autoimport" */ + +/** + * @defgroup locale-api Locale API functions for source and target strings + * @{ + */ + +/** + * Get information about text groups. + * + * Locale textgroups may use either the 'source' or the 'location' field as + * a key to determine unique strings and may opt into or out of caching. + */ +function locale_textgroup($group = NULL, $key = NULL) { + static $textgroups; + + if (!isset($textgroups)) { + $groups = module_invoke_all('locale', 'groups'); + } + + if ($group && $key) { + return isset($textgroups[$group][$key]) ? $textgroups[$group][$key] : NULL; + } + elseif ($group) { + return isset($textgroups[$group]) ? $textgroups[$group] : NULL; + } + else { + return $textgroups; + } +} + +/** + * Get source for a string, given a key and a value. + */ +function locale_get_source($textgroup, $value, $version = VERSION) { + $field = locale_textgroup($textgroup, 'key'); + return locale_source_load(NULL, array( + 'textgroup' => $textgroup, + $field => $value, + 'version' => $version, + )); +} + +/** + * Load a locale source record from the database. + * + * @param $lid + * The ID of the record to load. + * @param $fields + * An array of fields to return. If null, all fields will be returned. + * @return + * A matching record, or false on failure. + */ +function locale_source_load($lid, $conditions = array(), $fields = array()) { + if ($lid) { + $conditions = array('ls.lid' => $lid); + } + else { + if (!isset($conditions['version'])) { + $conditions['ls.version'] = VERSION; + } + } + return drupal_load_record('locales_source', 'ls', $conditions, $fields); +} + +/** + * Load one or more locale source records from the database. + * + * @param $lids + * An array of IDs of the records to load. + * @param $conditions + * An array field-value pairs to match. Values may be arrays, in which case + * matching will use the 'IN' operator. + * @param $fields + * An array of fields to return. If null, all fields will be returned. + * @return + * An array of all matching records, or false on failure. + */ +function locale_source_load_multiple($lids = array(), $conditions = array(), $fields = array()) { + if (!empty($lids)) { + $conditions['lid'] = $lids; + } + return drupal_load_records('locales_source', 'ls', $conditions, $fields); +} + +/** + * Save a locale source record to the database. + * + * @param $source + * A translation source object. + * @return + * SAVED_NEW, SAVED_UPDATED, or FALSE on failure. + */ +function locale_source_save(&$source) { + // Invalidate cache if this text group is cacheable + if (locale_textgroup($source->textgroup, 'cache')) { + variable_set('locale_rebuild_' . $source->textgroup, 1); + } + + return drupal_write_record('locales_source', $source, isset($source->lid) ? array('lid') : array()); +} + +/** + * Delete one or more locale source records from the database. Optionally, also + * delete associated target strings. + * + * @param $lids + * An array of IDs of the records to delete. + * @param $conditions + * An array field-value pairs to match. Values may be arrays, in which case + * matching will use the 'IN' operator. + * @param $delete_targets + * Boolean, whether to delete all associated target strings. + */ +function locale_source_delete($lids = NULL, $conditions = array(), $delete_targets = TRUE) { + // Accept one or an array of ID values. + if (!empty($lids)) { + $conditions['lid'] = $lids; + } + // If requested and deleting by ID, remove all associated targets. + if ($delete_targets && isset($conditions['lid'])) { + locale_target_delete($conditions['lid']); + } + // Remove the sources. + drupal_delete_records('locales_source', $conditions); +} + +/** + * Load a locale target record from the database. + * + * @param $lid + * The ID of the record to load. + * @param $langcode + * The language code of the record to load. + * @param $plural + * The plural value of the record to load. + * @param $fields + * An array of fields to return. If null, all fields will be returned. + * @return + * A matching record, or false on failure. + */ +function locale_target_load($lid, $langcode, $plural = 0, $fields = array()) { + return drupal_load_record('locales_target', 'lt', array('lt.lid' => $lid, 'lt.language' => $langcode, 'lt.plural' => $plural), $fields); +} + +/** + * Load one or more locale target records from the database. + * + * @param $conditions + * An array field-value pairs to match. Values may be arrays, in which case + * matching will use the 'IN' operator. + * @param $fields + * An array of fields to return. If null, all fields will be returned. + * @return + * An array of all matching records, or false on failure. + */ +function locale_target_load_multiple($conditions, $fields = array()) { + return drupal_load_records('locales_target', 'lt', $conditions, $fields); +} + +/** + * Save a locale target record to the database. + * + * @param $target + * A translation target object. + * @return + * SAVED_NEW, SAVED_UPDATED, or false on failure. + */ +function locale_target_save(&$target) { + if (!isset($target->plural)) { + $target->plural = 0; + } + $existing = locale_target_load($target->lid, $target->language, $target->plural); + if (!empty($existing)) { + return drupal_write_record('locales_target', $target, array('lid', 'language', 'plural')); + } + else { + return drupal_write_record('locales_target', $target); + } +} + +/** + * Delete one or more locale target records from the database. + * + * @param $lids + * An array of IDs of the records to delete. + * @param $conditions + * An array field-value pairs to match. Values may be arrays, in which case + * matching will use the 'IN' operator. + * @return + * An array of all matching records, or false on failure. + */ +function locale_target_delete($lids = NULL, $conditions = array()) { + // Accept one or an array of ID values. + if (!empty($lids)) { + $conditions['lid'] = $lids; + } + drupal_delete_records('locales_target', $conditions); +} + +/** + * Fetch a translation for a source string. + * + * @param $value + * The key value of the source to translate. May be either the source string + * or its location, depending on which key is used by the textgroup. + * @param $textgroup + * Text group that the source string belongs to. + * @param $langcodes + * Language code of the target string. If an array of codes is given, the first + * matched language will be returned. + * @param $location + * Location of the source string. If NULL, any matching string in the given + * textgroup will be accepted as the source to be matched. + * @param $plural + * Plural value of the target to be returned. + * @param $return_source + * Boolean: whether to return the original string if no translation is found. + * @return + * A requested translation, the original string if no translation was found, or + * false if no translation was found and $return_source is false. + */ +function locale_get_translation($value, $textgroup, $version = VERSION, $langcodes = NULL, $plural = 0, $return_source = TRUE) { + global $language; + + // If no language specified, use the current language. + if (is_null($langcodes)) { + $langcodes = $language->language; + } + + if ($field = locale_textgroup($textgroup, 'key')) { + $query = db_select('locales_source', 'ls'); + $alias = $query->leftJoin('locales_target', 'lt', 'ls.lid = lt.lid'); + $translations = $query->condition('lt.plural', $plural) + ->condition('ls.textgroup', $textgroup) + ->condition($alias . '.language', $langcodes, is_array($langcodes) ? 'IN' : '=') + ->condition('ls.' . $field, $value) + ->fields($alias, array('language', 'translation')) + ->fields('ls', array('source')) + ->execute() + ->fetchAll(); + } + // If we have an array of request languages, return the first found. + if (is_array($langcodes)) { + foreach ($langcodes as $langcode) { + foreach ($translations as $translation) { + if ($translation->language == $langcode) { + return $translation->translation; + } + } + } + } + elseif (is_array($translations)) { + $translation = current($translations); + if ($translation->language == $langcodes) { + return $translation->translation; + } + else { + if ($return_source) { + if ($translation->source) { + return $translation->source; + } + if ($field == 'source') { + return $value; + } + } + } + } + return FALSE; +} + +/** + * @} End of "locale-api" + */ Index: modules/locale/locale.module =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v retrieving revision 1.237 diff -u -p -r1.237 locale.module --- modules/locale/locale.module 5 Feb 2009 00:32:46 -0000 1.237 +++ modules/locale/locale.module 5 Feb 2009 07:41:36 -0000 @@ -196,7 +196,18 @@ function locale_perm() { function locale_locale($op = 'groups') { switch ($op) { case 'groups': - return array('default' => t('Built-in interface')); + return array( + 'default' => array( + 'name' => t('Built-in interface'), + 'index' => 'source', + 'cache' => TRUE + ), + 'text' => array( + 'name' => t('Long texts from modules'), + 'index' => 'location', + 'cache' => TRUE, + ), + ); } } Index: modules/locale/locale.test =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v retrieving revision 1.16 diff -u -p -r1.16 locale.test --- modules/locale/locale.test 5 Feb 2009 00:32:47 -0000 1.16 +++ modules/locale/locale.test 5 Feb 2009 07:41:37 -0000 @@ -381,3 +381,92 @@ class LanguageSwitchingFunctionalTest ex $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language anchor is marked as active on the language switcher block')); } } + +/** + * Tests for locale CRUD API functions. + */ +class LocaleApiTest extends DrupalWebTestCase { + 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'); + 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 = new StdClass(); + $source->location = 'test'; + $source->textgroup = 'text'; + $source->source = 'test'; + $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/settings/language/add', $edit, t('Add language')); + + // Create a locale target string. + $target = new StdClass(); + $target->lid = current($lids); + $target->translation = 'analyse'; + $target->language = 'fr'; + $insert_result = locale_target_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_target_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_target_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')); + + // Load a translation. + $translation = locale_get_translation($source->source, $source->textgroup, $source->location, $target->language); + $this->assertTrue($translation == 'tester', t('Translation found with locale_get_translation()')); + $translation = locale_get_translation($source->source, $source->textgroup, $source->location, 'es'); + $this->assertTrue($translation == $source->source, t('Original string returned when missing translation requested with locale_get_translation()')); + + // Delete source and target strings. + locale_source_delete($lids); + $sources = locale_source_load_multiple($lids); + $this->assertTrue(empty($sources), t('Locale source strings deleted')); + } + +}