Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.869
diff -u -p -r1.869 common.inc
--- includes/common.inc	18 Feb 2009 15:07:26 -0000	1.869
+++ includes/common.inc	19 Feb 2009 13:57:03 -0000
@@ -1086,16 +1086,13 @@ function fix_gpc_magic() {
  * @see st()
  * @see get_t()
  *
+ * After string translation the arguments if any will be replaced
+ * @see drupal_replace_string()
+ *
  * @param $string
  *   A string containing the English string to translate.
  * @param $args
- *   An associative array of replacements to make after translation. Incidences
- *   of any key in this array are replaced with the corresponding value. Based
- *   on the first character of the key, the value is escaped and/or themed:
- *    - !variable: inserted as is
- *    - @variable: escape plain text to HTML (check_plain)
- *    - %variable: escape text and theme as a placeholder for user-submitted
- *      content (check_plain + theme_placeholder)
+ *   An associative array of replacements to make after translation.
  * @param $langcode
  *   Optional language code to translate to a language other than what is used
  *   to display the page.
@@ -1123,32 +1120,52 @@ function t($string, $args = array(), $la
   }
   // Translate with locale module if enabled.
   elseif (function_exists('locale') && $langcode != 'en') {
-    $string = locale($string, $langcode);
-  }
-  if (empty($args)) {
-    return $string;
+    $string = locale('default', $string, $langcode);
   }
-  else {
-    // Transform arguments before inserting them.
-    foreach ($args as $key => $value) {
-      switch ($key[0]) {
-        case '@':
-          // Escaped only.
-          $args[$key] = check_plain($value);
-          break;
 
-        case '%':
-        default:
-          // Escaped and placeholder.
-          $args[$key] = theme('placeholder', $value);
-          break;
+  return drupal_replace_string($string, $args);
+}
 
-        case '!':
-          // Pass-through.
-      }
-    }
-    return strtr($string, $args);
+/**
+ * This will translate strings using textgroup and location context.
+ *
+ * When translating user defined strings we cannot rely on the source string to
+ * do the translation as it may change, maybe just to fix a typo and it would
+ * invalidate the translations.
+ * @see t()
+ *
+ * The default language for these strings will be the site default language
+ * instead of being always English like for t()
+ *
+ * After string translation the arguments if any will be replaced.
+ * @see drupal_replace_string()
+ *
+ * @param $textgroup
+ *   The locale text group this string belongs to.
+ * @param $key
+ *   String id, unique inside the text group.
+ * @param $string
+ *   A string in the default language to translate.
+ * @param $args
+ *   An associative array of replacements to make after translation.
+ * @param $langcode
+ *   Optional language code to translate to a language other than what is used.
+ *   to display the page.
+ * @return
+ *   The translated string.
+ */
+function tt($textgroup, $key, $string, $args = array(), $langcode = NULL) {
+  global $language;
+
+  if (!isset($langcode)) {
+    $langcode = !empty($language->language) ? $language->language : 'en';
+  }
+
+  if (function_exists('locale') && $langcode != language_default('language')) {
+    $string = locale($textgroup, $key, $langcode, $string);
   }
+
+  return drupal_replace_string($string, $args);
 }
 
 /**
@@ -4230,3 +4247,44 @@ function _drupal_flush_css_js() {
   }
   variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19));
 }
+
+/**
+ * Replace string with arguments
+ *
+ * @param $string
+ *   A plain string containing to be replaced with args.
+ * @param $args
+ *   An associative array of replacements to make after translation. Incidences
+ *   of any key in this array are replaced with the corresponding value. Based
+ *   on the first character of the key, the value is escaped and/or themed:
+ *    - !variable: inserted as is
+ *    - @variable: escape plain text to HTML (check_plain)
+ *    - %variable: escape text and theme as a placeholder for user-submitted
+ *      content (check_plain + theme_placeholder)
+ */
+function drupal_replace_string($string, $args) {
+  if (empty($args)) {
+    return $string;
+  }
+  else {
+    // Transform arguments before inserting them.
+    foreach ($args as $key => $value) {
+      switch ($key[0]) {
+        case '@':
+          // Escaped only.
+          $args[$key] = check_plain($value);
+          break;
+
+        case '%':
+        default:
+          // Escaped and placeholder.
+          $args[$key] = theme('placeholder', $value);
+          break;
+
+        case '!':
+          // Pass-through.
+      }
+    }
+    return strtr($string, $args);
+  }
+}
Index: includes/locale.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/locale.inc,v
retrieving revision 1.204
diff -u -p -r1.204 locale.inc
--- includes/locale.inc	18 Feb 2009 15:07:27 -0000	1.204
+++ includes/locale.inc	19 Feb 2009 13:57:06 -0000
@@ -492,10 +492,9 @@ function locale_languages_configure_form
  */
 function locale_translate_overview_screen() {
   $languages = language_list('language', TRUE);
-  $groups = module_invoke_all('locale', 'groups');
-
+  $groups = locale_textgroup(NULL, 'name');
   // Build headers with all groups in order.
-  $headers = array_merge(array(t('Language')), array_values($groups));
+  $headers = array_merge(array(t('Language')), $groups);
 
   // Collect summaries of all source strings in all groups.
   $sums = db_query("SELECT COUNT(*) AS strings, textgroup FROM {locales_source} GROUP BY textgroup");
@@ -571,7 +570,7 @@ function locale_translation_filters() {
     'options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
   );
 
-  $groups = module_invoke_all('locale', 'groups');
+  $groups = locale_textgroup(NULL, 'name');
   $filters['group'] = array(
     'title' => t('Limit search to'),
     'options' => array_merge(array('all' => t('All text groups')), $groups),
@@ -708,7 +707,7 @@ function locale_translate_import_form() 
   $form['import']['group'] = array('#type' => 'radios',
     '#title' => t('Text group'),
     '#default_value' => 'default',
-    '#options' => module_invoke_all('locale', 'groups'),
+    '#options' => locale_textgroup(NULL, 'name'),
     '#description' => t('Imported translations will be added to this text group.'),
   );
   $form['import']['mode'] = array('#type' => 'radios',
@@ -801,7 +800,7 @@ function locale_translate_export_po_form
   $form['export']['group'] = array('#type' => 'radios',
     '#title' => t('Text group'),
     '#default_value' => 'default',
-    '#options' => module_invoke_all('locale', 'groups'),
+    '#options' => locale_textgroup(NULL, 'name'),
   );
   $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
   return $form;
@@ -820,7 +819,7 @@ function locale_translate_export_pot_for
   $form['export']['group'] = array('#type' => 'radios',
     '#title' => t('Text group'),
     '#default_value' => 'default',
-    '#options' => module_invoke_all('locale', 'groups'),
+    '#options' => locale_textgroup(NULL, 'name'),
   );
   $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
   // Reuse PO export submission callback.
@@ -855,7 +854,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 +950,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;
+      $translation->language = $key;
+      locale_translation_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_translation_delete(array('lid' => $lid, 'language' => $key));
     }
 
     // Force JavaScript translation file recreation for this language.
@@ -2127,7 +2124,7 @@ function _locale_translate_seek() {
 
   $result = pager_query($sql, 50, 0, NULL, $arguments);
 
-  $groups = module_invoke_all('locale', 'groups');
+  $groups = locale_textgroup(NULL, 'name');
   $header = array(t('Text group'), t('String'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2'));
   $arr = array();
   while ($locale = db_fetch_object($result)) {
@@ -2735,3 +2732,4 @@ function _locale_batch_language_finished
 /**
  * @} End of "locale-autoimport"
  */
+
Index: modules/locale/locale.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.api.php,v
retrieving revision 1.2
diff -u -p -r1.2 locale.api.php
--- modules/locale/locale.api.php	9 Dec 2008 11:36:03 -0000	1.2
+++ modules/locale/locale.api.php	19 Feb 2009 13:57:06 -0000
@@ -14,13 +14,43 @@
 /**
  * Allows modules to define their own text groups that can be translated.
  *
+ * For 'groups' operation it should return an array of information for different
+ * text groups. Each item in the array should contain:
+ *
+ * 'name'
+ *    Non localized text group name for display on the translation UI. This
+ *    string cannot be localized here because that will lead to infinite
+ *    recursion.
+ * 'index'
+ *    Field name from locales_source table to use as default index
+ * 'cache'
+ *    Optional. Enable caching for this text group. If FALSE itmeans no caching.
+ *    Alternatively it may contain a query condition (string) to fill in the
+ *    cache.
+ * 'version'
+ *    Optional. For some text groups we keep track of source string versions in
+ *    order to be able to do some clean up (deleting old strings). If so, it
+ *    will contain the current version.
+ * 'update'
+ *    Optional. If set to TRUE source strings will be kept up to date while
+ *    translating. This has some performance impact so ideally the source
+ *    strings for a text group will be kept up to date some other way.
+ *
  * @param $op
  *   Type of operation. Currently, only supports 'groups'.
  */
 function hook_locale($op = 'groups') {
   switch ($op) {
     case 'groups':
-      return array('custom' => t('Custom'));
+      return array(
+        'custom' => array(
+          'name' => 'Custom text group',
+          'index' => 'location',
+          'cache' => FALSE,
+          'version' => VERSION,
+          'update' => TRUE,
+        ),
+      );
   }
 }
 
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	19 Feb 2009 13:57:07 -0000
@@ -196,7 +196,15 @@ function locale_perm() {
 function locale_locale($op = 'groups') {
   switch ($op) {
     case 'groups':
-      return array('default' => t('Built-in interface'));
+      return array(
+        'default' => array(
+          'name' => 'Built-in interface',
+          'index' => 'source', // This group is indexed by source string.
+          'cache' => 'LENGTH (s.source) < 75', // Condition to fill in the cache.
+          'version' => VERSION, // This group's string may change with Drupal version.
+          'update' => TRUE, // Update sources as we are translating.
+        ),
+      );
   }
 }
 
@@ -321,90 +329,89 @@ function locale_theme() {
 }
 
 // ---------------------------------------------------------------------------------
-// Locale core functionality
+// Locale core functionality.
 
 /**
- * Provides interface translation services.
+ * Provides translation services.
  *
- * This function is called from t() to translate a string if needed.
+ * This function will find translations for any of the text groups
+ * searching by any field (source, location).
  *
+ * @param $textgroup
+ *   Text group to search for.
+ * @param $value
+ *   Unique key for this string inside the text group.
  * @param $string
- *   A string to look up translation for. If omitted, all the
- *   cached strings will be returned in all languages already
- *   used on the page.
+ *   The source string, that may be used for updating.
  * @param $langcode
  *   Language code to use for the lookup.
+ * @param $index
+ *   Field to use as index that will be matched against $value.
  * @param $reset
  *   Set to TRUE to reset the in-memory cache.
+ * @return
+ *   Translated string if found, source string otherwise.
  */
-function locale($string = NULL, $langcode = NULL, $reset = FALSE) {
+function locale($textgroup = NULL, $value = NULL, $langcode = NULL, $string = NULL, $index = NULL, $reset = FALSE) {
   global $language;
   static $locale_t;
+  static $indexes;
 
   if ($reset) {
     // Reset in-memory cache.
     $locale_t = NULL;
   }
-
-  if (!isset($string)) {
-    // Return all cached strings if no string was specified
+  // Return all cached strings or all the text group if parameters missing.
+  if (!isset($textgroup)) {
     return $locale_t;
   }
+  elseif (!isset($value)) {
+    return isset($locale_t[$textgroup]) ? $locale_t[$textgroup] : array();
+  }
 
   $langcode = isset($langcode) ? $langcode : $language->language;
 
-  // Store database cached translations in a static var.
-  if (!isset($locale_t[$langcode])) {
-    $locale_t[$langcode] = array();
-    // Disabling the usage of string caching allows a module to watch for
-    // the exact list of strings used on a page. From a performance
-    // perspective that is a really bad idea, so we have no user
-    // interface for this. Be careful when turning this option off!
-    if (variable_get('locale_cache_strings', 1) == 1) {
-      if ($cache = cache_get('locale:' . $langcode, 'cache')) {
-        $locale_t[$langcode] = $cache->data;
-      }
-      else {
-        // Refresh database stored cache of translations for given language.
-        // We only store short strings used in current version, to improve
-        // performance and consume less memory.
-        $result = db_query("SELECT s.source, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.textgroup = 'default' AND s.version = '%s' AND LENGTH(s.source) < 75", $langcode, VERSION);
-        while ($data = db_fetch_object($result)) {
-          $locale_t[$langcode][$data->source] = (empty($data->translation) ? TRUE : $data->translation);
-        }
-        cache_set('locale:' . $langcode, $locale_t[$langcode]);
-      }
-    }
+  if (!isset($indexes)) {
+    $indexes = locale_textgroup(NULL, 'index');
   }
 
-  // If we have the translation cached, skip checking the database
-  if (!isset($locale_t[$langcode][$string])) {
+  $index = $index ? $index : $indexes[$textgroup];
 
-    // We do not have this translation cached, so get it from the DB.
-    $translation = db_fetch_object(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 = '%s' WHERE s.source = '%s' AND s.textgroup = 'default'", $langcode, $string));
-    if ($translation) {
-      // We have the source string at least.
-      // Cache translation string or TRUE if no translation exists.
-      $locale_t[$langcode][$string] = (empty($translation->translation) ? TRUE : $translation->translation);
+  if (!$string && $index == 'source') {
+    $string = $value;
+  }
+  // Store database cached translations in a static var.
+  if (!isset($locale_t[$textgroup][$index][$langcode])) {
+    $locale_t[$textgroup][$index][$langcode] = _locale_get_cache($textgroup, $index, $langcode);
+  }
 
-      if ($translation->version != VERSION) {
-        // This is the first use of this string under current Drupal version. Save version
+  // If we have the translation cached, skip checking the database.
+  if (!isset($locale_t[$textgroup][$index][$langcode][$value])) {
+    // If the group has auto update mode, retrieve full translation and check
+    // some more stuff.
+    $update = locale_textgroup($textgroup, 'update');
+    $translation = locale_translation_load(array('textgroup' => $textgroup, $index => $value, 'language' => $langcode), $update);
+    // Cache translation string or TRUE if no translation exists.
+    $locale_t[$textgroup][$index][$langcode][$value] = empty($translation->translation) ? TRUE : $translation->translation;
+    if ($string && $update) {
+      // Some groups use version, some groups dosn't.
+      $version = locale_textgroup($textgroup, 'version');
+      if (!$translation && $string) {
+        $source = array('textgroup' => $textgroup, 'source' => $string, 'version' => $version, 'location' => request_uri());
+        $source += array($index => $value);
+        locale_source_save($source);
+      }
+      elseif ($translation && $version && $translation->version != $version) {
+        // This is the first use of this string under current 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_query("UPDATE {locales_source} SET version = '%s' WHERE lid = %d", VERSION, $translation->lid);
-        cache_clear_all('locale:', 'cache', TRUE);
+        db_query("UPDATE {locales_source} SET version = '%s' WHERE lid = %d", $version, $translation->lid);
+        variable_set('locale_rebuild_' . $textgroup, 1);
       }
     }
-    else {
-      // We don't have the source string, cache this as untranslated.
-      db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', 'default', '%s')", request_uri(), $string, VERSION);
-      $locale_t[$langcode][$string] = TRUE;
-      // Clear locale cache so this string can be added in a later request.
-      cache_clear_all('locale:', 'cache', TRUE);
-    }
   }
 
-  return ($locale_t[$langcode][$string] === TRUE ? $string : $locale_t[$langcode][$string]);
+  return $locale_t[$textgroup][$index][$langcode][$value] === TRUE ? $string : $locale_t[$textgroup][$index][$langcode][$value];
 }
 
 /**
@@ -573,8 +580,8 @@ function locale_block_list() {
 /**
  * Implementation of hook_block_view().
  *
- * Displays a language switcher. Translation links may be provided by other modules.
- * Only show if we have at least two languages and language dependent
+ * Displays a language switcher. Translation links may be provided by other
+ * modules. Only show if we have at least two languages and language dependent
  * web addresses, so we can actually link to other language versions.
  */
 function locale_block_view($delta = '') {
@@ -601,6 +608,330 @@ function locale_block_view($delta = '') 
 }
 
 /**
+ * @defgroup locale-api Locale API functions for source and target strings.
+ * @{
+ */
+
+/**
+ * Get information about text groups.
+ *
+ * This will return information for all text groups if not group specified, all
+ * information for that group if specified, or the value of a group's property
+ * if $property is passed.
+ *
+ * Alternatively, passing only the property name will return an array of this
+ * property indexed by text group key.
+ *
+ * @param $group
+ *   Optional group name
+ * @param $property
+ *   Optional value to query
+ */
+function locale_textgroup($group = NULL, $property = NULL) {
+  static $textgroups;
+
+  if (!isset($textgroups)) {
+    $textgroups = module_invoke_all('locale', 'groups');
+  }
+
+  if ($group && $property) {
+    return isset($textgroups[$group][$property]) ? $textgroups[$group][$property] : NULL;
+  }
+  elseif ($group) {
+    return isset($textgroups[$group]) ? $textgroups[$group] : NULL;
+  }
+  elseif ($property) {
+    $list = array();
+    foreach ($textgroups as $key => $data) {
+      isset($data[$property]) ? ($list[$key] = $data[$property]) : NULL;
+    }
+    // If the property is text group name, localize the list.
+    if ($property == 'name') {
+      $list = array_map('t', $list);
+    }
+    return $list;
+  }
+  else {
+    return $textgroups;
+  }
+}
+
+/**
+ * Load a source string record.
+ *
+ * @param $conditions
+ *   A source lid or an array of key => value pairs.
+ * @return
+ *   Source string.
+ */
+function locale_source_load($conditions) {
+  $records = locale_source_load_multiple($conditions);
+  return !empty($records) ? current($records) : FALSE;
+}
+
+/**
+ * Get source for a string, given a key and a value.
+ *
+ * @param $conditions
+ *   A source lid, 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($conditions) {
+  if (is_numeric($conditions) || is_numeric(key($conditions))) {
+    $conditions = array('lid' => $conditions);
+  }
+
+  $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.
+ *
+ * @param $string
+ *   Object representing a source string.
+ */
+function locale_source_save(&$source) {
+  // Ensure we're working with an object.
+  $source = (object) $source;
+  // Invalidate cache if this text group is cacheable.
+  if (locale_textgroup($source->textgroup, 'cache')) {
+    variable_set('locale_rebuild_' . $source->textgroup, 1);
+  }
+  if (!empty($source->lid)) {
+    return drupal_write_record('locales_source', $source, 'lid');
+  }
+  else {
+    return drupal_write_record('locales_source', $source);
+  }
+}
+
+/**
+ * Load a translation string record.
+ *
+ * @param $conditions
+ *   A source lid 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($conditions, $get_source = FALSE) {
+  $records = locale_translation_load_multiple($conditions, $get_source);
+  return !empty($records) ? current($records) : FALSE;
+}
+
+/**
+ * Load one or more translations.
+ *
+ * @param $conditions
+ *   A source lid or 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($conditions, $get_source = FALSE) {
+  // Accept a single lid or an array of lids.
+  if (is_numeric($conditions) || 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', array('lid', 'version'));
+    $join = 'leftJoin';
+  }
+  else {
+    $join = 'join';
+  }
+  $query->$join('locales_target', 't', 's.lid = t.lid AND t.language = :langcode', array(':langcode' => $conditions['language']));
+  unset($conditions['language']);
+  $query->fields('t');
+  foreach ($conditions as $field => $value) {
+    $query->condition('s.' . $field, $value, is_array($value) ? 'IN' : '=');
+  }
+  return $query->execute()->fetchAllAssoc('lid');
+}
+
+/**
+ * Save a translation.
+ *
+ * If the string has enough data and it doesn't have a string id, it will check
+ * and create the source too if it doesn't exist.
+ *
+ * @param $string
+ *   Object representing a string translation.
+ * @return
+ *   SAVED_NEW, SAVED_UPDATED, or FALSE on failure.
+ */
+function locale_translation_save(&$string) {
+  // If a current translation exists, just update the record, find it if not
+  // given.
+  if (empty($string->lid)) {
+    if (!empty($string->textgroup)) {
+      $key = locale_textgroup($string->textgroup, 'key');
+      if (!empty($string->$key)) {
+        if ($source = locale_source_load(array('textgroup' => $string->textgroup, $key => $string->$key))) {
+          $string->lid = $source->lid;
+        }
+        elseif (!empty($string->source)) {
+          // Create source if it doesn't exist and we have enough data.
+          locale_source_save($string);
+        }
+      }
+    }
+  }
+
+  // Check again for existing source and translation.
+  if (!empty($string->lid)) {
+    // Invalidate cache if this text group is cacheable.
+    if (!empty($string->textgroup) && locale_textgroup($string->textgroup, 'cache')) {
+      variable_set('locale_rebuild_' . $string->textgroup, 1);
+    }
+    $existing = locale_translation_load(array('lid' => $string->lid, 'language' => $string->language));
+    if (!empty($existing)) {
+      return drupal_write_record('locales_target', $string, array('lid', 'language', 'plural'));
+    }
+    else {
+      return drupal_write_record('locales_target', $string);
+    }
+  }
+  else {
+    return FALSE;
+  }
+}
+
+/**
+ * Delete one or more locale source records from the database. Optionally, also
+ * delete associated target strings.
+ *
+ * @param $conditions
+ *   A single lid, 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($conditions, $delete_translations = TRUE) {
+  // Accept a single lid or an array of lids.
+  if (is_numeric($conditions) || is_numeric(key($conditions))) {
+    $conditions = array('lid' => $conditions);
+  }
+  // If requested and deleting by ID, remove all associated targets.
+  if ($delete_translations) {
+    locale_translation_delete($conditions);
+  }
+  // Remove the sources.
+  $query = db_delete('locales_source');
+  foreach ($conditions as $field => $value) {
+    $query->condition($field, $value, is_array($value) ? 'IN' : '=');
+  }
+  $query->execute();
+}
+
+/**
+ * Delete one or more locale target records from the database.
+ *
+ * @param $conditions
+ *   A single lid, 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($conditions) {
+  // Accept a single lid or an array of lids.
+  if (is_numeric($conditions) || 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, array('lid', 'translation', 'language', 'plural'))) {
+      $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->where('lid IN (' . (string)$subquery . ')', $subquery->getArguments());
+  }
+  $query->execute();
+}
+
+/**
+ * @} End of "locale-api"
+ */
+
+/**
+ * Helper function to load locale cache.
+ *
+ * @param $textgroup
+ *   Text group we want to get the cache for.
+ * @param $langcode
+ *   Target language to load.
+ * @param $maxlength
+ *   Max length of the strings to load in the cache.
+ * @param $rebuild
+ *   Whether to rebuild the whole cache for this text group.
+ */
+function _locale_get_cache($textgroup, $index, $langcode, $rebuild = FALSE) {
+  $locale = array();
+
+  // See whether we need to rebuild and reset the variable if so.
+  if (variable_get('locale_rebuild_' . $textgroup, 0)) {
+    $rebuild = TRUE;
+    variable_del('locale_rebuild_' . $textgroup);
+  }
+  // Check whether this text group is cacheable.
+  $cache_condition = locale_textgroup($textgroup, 'cache');
+  // Disabling the usage of string caching allows a module to watch for
+  // the exact list of strings used on a page. From a performance
+  // perspective that is a really bad idea, so we have no user
+  // interface for this. Be careful when turning this option off!
+  if ($cache_condition && variable_get('locale_cache_strings', 1) == 1) {
+
+    $cache_key = 'locale:' . $textgroup . ':' . $langcode;
+    $cache = cache_get($cache_key, 'cache');
+    if (!$rebuild && $cache) {
+      $locale = $cache->data;
+    }
+    else {
+      // Refresh database stored cache of translations for given language.
+      // We only store short strings used in current version, to improve
+      // performance and consume less memory.
+      $query = db_select('locales_source', 's');
+      $query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :langcode', array(':langcode' => $langcode));
+      $query->addField('s', $index);
+      $query->addField('t', 'translation');
+      $query->condition('textgroup', $textgroup);
+      // If this text group is versioned, add such condition.
+      if ($version = locale_textgroup($textgroup, 'version')) {
+        $query->condition('version', $version);
+      }
+      // Check limit for caching.
+      if (is_string($cache_condition)) {
+        $query->where($cache_condition);
+      }
+      $result = $query->execute();
+      foreach ($result as $data) {
+        $locale[$data->$index] = empty($data->translation) ? TRUE : $data->translation;
+      }
+      cache_set($cache_key, $locale);
+    }
+  }
+  return $locale;
+}
+
+/**
  * Theme locale translation filter selector.
  *
  * @ingroup themeable
Index: modules/locale/locale.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v
retrieving revision 1.17
diff -u -p -r1.17 locale.test
--- modules/locale/locale.test	13 Feb 2009 00:45:18 -0000	1.17
+++ modules/locale/locale.test	19 Feb 2009 13:57:08 -0000
@@ -65,7 +65,7 @@ class LocaleTranslationFunctionalTest ex
     // Add string.
     t($name, array(), $langcode);
     // Reset locale cache.
-    locale(NULL, NULL, TRUE);
+    locale(NULL, NULL, NULL, NULL, NULL, TRUE);
     $this->assertText($langcode, 'Language code found');
     $this->assertText($name, 'Name found');
     $this->assertText($native, 'Native found');
@@ -526,3 +526,94 @@ 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', '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 = new stdClass();
+    $source->location = 'test';
+    $source->textgroup = 'custom';
+    $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_translation_save($target);
+    // Awaiting http://drupal.org/node/369423.
+    // $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);
+    // Awaiting http://drupal.org/node/369423.
+    // $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(array('lid' => $target->lid, 'language' => $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 = tt($source->textgroup, $source->location, $source->source, array(), $target->language);
+    $this->assertTrue($translation == 'tester', t('Translation found with tt()'));
+    $translation = tt($source->textgroup, $source->location, $source->source, array(), 'es');
+    $this->assertTrue($translation == $source->source, t('Original string returned when missing translation requested with tt()'));
+
+    // Delete source and target strings.
+    locale_source_delete($lids);
+    $sources = locale_source_load(array('lid' => $lids));
+    $this->assertTrue(empty($sources), t('Locale source strings deleted'));
+  }
+
+}
Index: modules/locale/tests/locale_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/tests/locale_test.module,v
retrieving revision 1.1
diff -u -p -r1.1 locale_test.module
--- modules/locale/tests/locale_test.module	22 Jan 2009 16:38:15 -0000	1.1
+++ modules/locale/tests/locale_test.module	19 Feb 2009 13:57:08 -0000
@@ -12,6 +12,12 @@
 function locale_test_locale($op = 'groups') {
   switch ($op) {
     case 'groups':
-      return array('custom' => t('Custom'));
+      return array(
+        'custom' => array(
+          'name' => 'Custom',
+          'index' => 'location',
+          'cache' => FALSE,
+        ),
+      );
   }
 }
