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 23 Feb 2009 14:12:51 -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.205 diff -u -p -r1.205 locale.inc --- includes/locale.inc 22 Feb 2009 17:55:29 -0000 1.205 +++ includes/locale.inc 23 Feb 2009 14:12:54 -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)) { @@ -2736,3 +2733,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 23 Feb 2009 14:12:55 -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 23 Feb 2009 14:12:56 -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.18 diff -u -p -r1.18 locale.test --- modules/locale/locale.test 22 Feb 2009 20:55:18 -0000 1.18 +++ modules/locale/locale.test 23 Feb 2009 14:12:57 -0000 @@ -214,7 +214,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, t('Language code found.')); $this->assertText($name, t('Name found.')); $this->assertText($native, t('Native found.')); @@ -407,7 +407,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->drupalLogout(); // Search for the name. @@ -1377,3 +1377,93 @@ class LocaleContentFunctionalTest extend } } +/** + * 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 23 Feb 2009 14:12:57 -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, + ), + ); } }