Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.641 diff -u -p -r1.641 common.inc --- includes/common.inc 15 May 2007 20:19:47 -0000 1.641 +++ includes/common.inc 19 May 2007 12:38:02 -0000 @@ -747,6 +747,28 @@ function t($string, $args = 0, $langcode } /** + * Dynamic object translation. + * + * @param $domain + * Text domain to search string in. For the 'built-in' domain, + * use the t() function. For other domains, use dt(). Domains + * should be defined using hook_locale(). + * @param $object_type + * The name of the object type as specified in hook_locale(). + * @param $object + * Object to translate. Localizable properties defined in hook_locale() + * are replaced with localized versions if available. + * @param $dt_language + * Optional language code to look up the string in. Defaults to the page language. + */ +function dt($domain, $object_type, $object, $dt_language = NULL) { + if (function_exists('locale_dynamic')) { + return locale_dynamic($domain, $object_type, $object, $dt_language); + } + return $object; +} + +/** * @defgroup validation Input validation * @{ * Functions to validate user input. Index: modules/aggregator/aggregator.module =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.module,v retrieving revision 1.339 diff -u -p -r1.339 aggregator.module --- modules/aggregator/aggregator.module 14 May 2007 13:43:33 -0000 1.339 +++ modules/aggregator/aggregator.module 19 May 2007 12:38:06 -0000 @@ -200,6 +200,21 @@ function aggregator_menu() { return $items; } +/** + * Implementation of hook_locale(). + */ +function aggregator_locale($op = 'groups') { + switch($op) { + case 'groups': + return array('aggregator' => t('Aggregator')); + case 'objects': + return array('aggregator' => array( + 'category' => array('cid', 'title', 'description'), + 'feed' => array('fid', 'title'), + )); + } +} + function aggregator_init() { drupal_add_css(drupal_get_path('module', 'aggregator') .'/aggregator.css'); } @@ -267,11 +282,11 @@ function aggregator_block($op = 'list', if (user_access('access news feeds')) { if ($op == 'list') { $result = db_query('SELECT cid, title FROM {aggregator_category} ORDER BY title'); - while ($category = db_fetch_object($result)) { + while ($category = dt('aggregator', 'category', db_fetch_object($result))) { $block['category-'. $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title)); } $result = db_query('SELECT fid, title FROM {aggregator_feed} ORDER BY fid'); - while ($feed = db_fetch_object($result)) { + while ($feed = dt('aggregator', 'feed', db_fetch_object($result))) { $block['feed-'. $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title)); } } @@ -299,7 +314,7 @@ function aggregator_block($op = 'list', list($type, $id) = explode('-', $delta); switch ($type) { case 'feed': - if ($feed = db_fetch_object(db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE fid = %d', $id))) { + if ($feed = dt('aggregator', 'feed', db_fetch_object(db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE fid = %d', $id)))) { $block['subject'] = check_plain($feed->title); $result = db_query_range('SELECT * FROM {aggregator_item} WHERE fid = %d ORDER BY timestamp DESC, iid DESC', $feed->fid, 0, $feed->block); $read_more = ''; @@ -307,7 +322,7 @@ function aggregator_block($op = 'list', break; case 'category': - if ($category = db_fetch_object(db_query('SELECT cid, title, block FROM {aggregator_category} WHERE cid = %d', $id))) { + if ($category = dt('aggregator', 'category', db_fetch_object(db_query('SELECT cid, title, block FROM {aggregator_category} WHERE cid = %d', $id)))) { $block['subject'] = check_plain($category->title); $result = db_query_range('SELECT i.* FROM {aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON ci.iid = i.iid WHERE ci.cid = %d ORDER BY i.timestamp DESC, i.iid DESC', $category->cid, 0, $category->block); $read_more = ''; @@ -421,14 +436,17 @@ function aggregator_form_category_submit function aggregator_save_category($edit) { if (!empty($edit['cid']) && !empty($edit['title'])) { db_query("UPDATE {aggregator_category} SET title = '%s', description = '%s' WHERE cid = %d", $edit['title'], $edit['description'], $edit['cid']); + module_invoke('locale', 'dynamic_update', 'aggregator', 'category', (object) $edit); } else if (!empty($edit['cid'])) { db_query('DELETE FROM {aggregator_category} WHERE cid = %d', $edit['cid']); + module_invoke('locale', 'dynamic_update', 'aggregator', 'category', (object) $edit, 'delete'); } else if (!empty($edit['title'])) { // A single unique id for bundles and feeds, to use in blocks - $next_id = db_next_id('{aggregator_category}_cid'); - db_query("INSERT INTO {aggregator_category} (cid, title, description, block) VALUES (%d, '%s', '%s', 5)", $next_id, $edit['title'], $edit['description']); + $edit['cid'] = db_next_id('{aggregator_category}_cid'); + db_query("INSERT INTO {aggregator_category} (cid, title, description, block) VALUES (%d, '%s', '%s', 5)", $edit['cid'], $edit['title'], $edit['description']); + module_invoke('locale', 'dynamic_update', 'aggregator', 'category', (object) $edit); } } @@ -467,7 +485,7 @@ function aggregator_form_feed($edit = ar $options = array(); $values = array(); $categories = db_query('SELECT c.cid, c.title, f.fid FROM {aggregator_category} c LEFT JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = %d ORDER BY title', $edit['fid']); - while ($category = db_fetch_object($categories)) { + while ($category = dt('aggregator', 'category', db_fetch_object($categories))) { $options[$category->cid] = check_plain($category->title); if ($category->fid) $values[] = $category->cid; } @@ -565,6 +583,7 @@ function aggregator_save_feed($edit) { } if ($edit['fid'] && $edit['title']) { db_query("UPDATE {aggregator_feed} SET title = '%s', url = '%s', refresh = %d WHERE fid = %d", $edit['title'], $edit['url'], $edit['refresh'], $edit['fid']); + module_invoke('locale', 'dynamic_update', 'aggregator', 'feed', (object) $edit); } else if ($edit['fid']) { $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $edit['fid']); @@ -576,11 +595,13 @@ function aggregator_save_feed($edit) { } db_query('DELETE FROM {aggregator_feed} WHERE fid = %d', $edit['fid']); db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $edit['fid']); + module_invoke('locale', 'dynamic_update', 'aggregator', 'feed', (object) $edit, 'delete'); } else if ($edit['title']) { // A single unique id for bundles and feeds, to use in blocks. $edit['fid'] = db_next_id('{aggregator_feed}_fid'); db_query("INSERT INTO {aggregator_feed} (fid, title, url, refresh, block) VALUES (%d, '%s', '%s', %d, 5)", $edit['fid'], $edit['title'], $edit['url'], $edit['refresh']); + module_invoke('locale', 'dynamic_update', 'aggregator', 'feed', (object) $edit); } if ($edit['title']) { // The feed is being saved, save the categories as well. @@ -1010,7 +1031,7 @@ function aggregator_view() { $header = array(t('Title'), t('Items'), t('Last update'), t('Next update'), array('data' => t('Operations'), 'colspan' => '3')); $rows = array(); - while ($feed = db_fetch_object($result)) { + while ($feed = dt('aggregator', 'feed', db_fetch_object($result))) { $rows[] = array(l($feed->title, "aggregator/sources/$feed->fid"), format_plural($feed->items, '1 item', '@count items'), ($feed->checked ? t('@time ago', array('@time' => format_interval(time() - $feed->checked))) : t('never')), ($feed->checked ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - time()))) : t('never')), l(t('edit'), "admin/content/aggregator/edit/feed/$feed->fid"), l(t('remove items'), "admin/content/aggregator/remove/$feed->fid"), l(t('update items'), "admin/content/aggregator/update/$feed->fid")); } $output .= theme('table', $header, $rows); @@ -1021,7 +1042,7 @@ function aggregator_view() { $header = array(t('Title'), t('Items'), t('Operations')); $rows = array(); - while ($category = db_fetch_object($result)) { + while ($category = dt('aggregator', 'category', db_fetch_object($result))) { $rows[] = array(l($category->title, "aggregator/categories/$category->cid"), format_plural($category->items, '1 item', '@count items'), l(t('edit'), "admin/content/aggregator/edit/category/$category->cid")); } $output .= theme('table', $header, $rows); @@ -1065,7 +1086,7 @@ function aggregator_page_last() { * Menu callback; displays all the items captured from a particular feed. */ function aggregator_page_source() { - $feed = db_fetch_object(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', arg(2))); + $feed = dt('aggregator', 'feed', db_fetch_object(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', arg(2)))); drupal_set_title(check_plain($feed->title)); $info = theme('aggregator_feed', $feed); @@ -1076,7 +1097,7 @@ function aggregator_page_source() { * Menu callback; displays all the items aggregated in a particular category. */ function aggregator_page_category() { - $category = db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2))); + $category = dt('aggregator', 'feed', db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2)))); drupal_add_feed(url('aggregator/rss/'. arg(2)), variable_get('site_name', 'Drupal') .' '. t('aggregator - @title', array('@title' => $category->title))); @@ -1099,7 +1120,7 @@ function aggregator_page_list($sql, $hea if ($categorize) { $categories_result = db_query('SELECT c.cid, c.title, ci.iid FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid AND ci.iid = %d', $item->iid); $selected = array(); - while ($category = db_fetch_object($categories_result)) { + while ($category = dt('aggregator', 'category', db_fetch_object($categories_result))) { if (!$done) { $categories[$category->cid] = check_plain($category->title); } @@ -1188,7 +1209,7 @@ function aggregator_page_list_submit($fo function aggregator_page_sources() { $result = db_query('SELECT f.fid, f.title, f.description, f.image, MAX(i.timestamp) AS last FROM {aggregator_feed} f LEFT JOIN {aggregator_item} i ON f.fid = i.fid GROUP BY f.fid, f.title, f.description, f.image ORDER BY last DESC, f.title'); $output = "
\n"; - while ($feed = db_fetch_object($result)) { + while ($feed = dt('aggregator', 'feed', db_fetch_object($result))) { $output .= '

'. check_plain($feed->title) ."

\n"; // Most recent items: @@ -1220,7 +1241,7 @@ function aggregator_page_rss() { // arg(2) is the passed cid, only select for that category $result = NULL; if (arg(2)) { - $category = db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2))); + $category = dt('aggregator', 'category', db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2)))); $url = '/categories/'. $category->cid; $title = ' '. t('in category') .' '. $category->title; $sql = 'SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = %d ORDER BY timestamp DESC, iid DESC'; @@ -1276,7 +1297,7 @@ function aggregator_page_opml($cid = NUL $output .= "\n"; $output .= "\n"; - while ($feed = db_fetch_object($result)) { + while ($feed = dt('aggregator', 'feed', db_fetch_object($result))) { $output .= '\n"; } @@ -1294,7 +1315,7 @@ function aggregator_page_categories() { $result = db_query('SELECT c.cid, c.title, c.description FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid LEFT JOIN {aggregator_item} i ON ci.iid = i.iid GROUP BY c.cid, c.title, c.description'); $output = "
\n"; - while ($category = db_fetch_object($result)) { + while ($category = dt('aggregator', 'category', db_fetch_object($result))) { $output .= '

'. check_plain($category->title) ."

\n"; if (variable_get('aggregator_summary_items', 3)) { $list = array(); @@ -1413,7 +1434,7 @@ function theme_aggregator_page_item($ite $result = db_query('SELECT c.title, c.cid FROM {aggregator_category_item} ci LEFT JOIN {aggregator_category} c ON ci.cid = c.cid WHERE ci.iid = %d ORDER BY c.title', $item->iid); $categories = array(); - while ($category = db_fetch_object($result)) { + while ($category = dt('aggregator', 'category', db_fetch_object($result))) { $categories[] = l($category->title, 'aggregator/categories/'. $category->cid); } if ($categories) { Index: modules/locale/locale.module =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v retrieving revision 1.172 diff -u -p -r1.172 locale.module --- modules/locale/locale.module 15 May 2007 20:19:47 -0000 1.172 +++ modules/locale/locale.module 19 May 2007 12:38:06 -0000 @@ -389,6 +389,154 @@ function locale_get_plural($count) { return $plurals[$count]; } +/** + * Dynamic object translation. + * + * @param $group + * Text group to search strings in. For the 'default' group, + * use the t() function. For other groups, use dt(). Groups + * should be defined using hook_locale(). + * @param $object_type + * The name of the object type as specified in hook_locale(). + * @param $object + * Object to translate. Localizable properties defined in hook_locale() + * are replaced with localized versions if available. + * @param $dt_language + * Optional language code to look up strings in. + * Defaults to the language used to generate the page. + * + * @todo + * - We could add a cache property to $object[$group][$object_type]['cache'] = TRUE | FALSE + * - An empty string may be a valid translation too!! Let's allow some flexibility. + */ +function locale_dynamic($group, $object_type, $object, $dt_language = NULL) { + global $language; + static $objects = NULL; + static $cache = array(); + + // Cache object metadata provided by modules for translation. + if (!isset($objects)) { + $objects = module_invoke_all('locale', 'objects'); + } + + // Fall back on default language if not instructed otherwise. + $default = language_default(); + if (!isset($dt_language)) { + $dt_language = $language->language; + } + + // Skip if default language or found no descriptor for the object. + if (($dt_language == $default->language) || !isset($objects[$group][$object_type])) { + return $object; + } + + // Allow groups to be translated with custom callbacks by contributed + // modules, enabling better caching tailored for specific needs. + if (isset($objects[$group]['#callback']) && function_exists($objects[$group]['#callback'])) { + return call_user_func($objects[$group]['#callback'], $group, $object_type, $object, $dt_language); + } + + // Build a list of locations searched for. + $properties = $objects[$group][$object_type]; + $idname = array_shift($properties); + $locations = array(); + foreach ($properties as $property) { + // Only try to look up properties present in the passed object. + if (isset($object->$property)) { + $location = $object_type .':'. $object->$idname .':'. $property; + // Try to find in cached properties first. + if (isset($cache[$dt_language][$group][$location])) { + // Nonexistent translations are cached as NULL. + $object->property = is_null($cache[$dt_language][$group][$location]) ? $object->property : $cache[$dt_language][$group][$location]; + } else { + $locations[] = $location; + // Expecting that we have no translation for this, or will + // replace this NULL later with a value. + $cache[$dt_language][$group][$location] = NULL; + } + } + } + + // We assume source strings will exist in the database, because + // locale_dynamic_update() should have been called earlier for + // the object. We search the database if not all properties were + // in our local cache. + if (count($locations)) { + $result = db_query("SELECT s.location, s.source, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE t.language = '%s' AND s.location IN ('". join("','", $locations) ."') AND s.textgroup = '%s'", $dt_language, $group); + + // Translate every object property for which we have translation. + while ($row = db_fetch_object($result)) { + list (,,$property) = explode(':', $row->location); + if ($object->$property == $row->source) { + $object->$property = $row->translation; + } + $cache[$dt_language][$group][$row->location] = $row->translation; + } + } + + return $object; +} + +/** + * Update dynamic strings from objects. + * Create new entries and remove old ones. + * + * @param $group + * Text group to save the strings in. This function should not be used for + * the 'default' group! Groups should be defined using hook_locale(). + * @param $object_type + * The name of the object type as specified in hook_locale(). + * @param $object + * Object to save. Localizable properties defined in hook_locale() are saved. + * @param $op + * 'update' (default) to create/update existing source strings. + * 'delete' to remove existing entries. + */ +function locale_dynamic_update($group, $object_type, $object, $op = 'update') { + static $objects = NULL; + + // Cache object metadata provided by modules for translation. + if (!isset($objects)) { + $objects = module_invoke_all('locale', 'objects'); + } + + // Return with error if no information found about this object type. + if (!isset($objects[$group][$object_type])) { + return FALSE; + } + + // Build a list of locations we need to work with. + $properties = $objects[$group][$object_type]; + $idname = array_shift($properties); + $locations = array(); + foreach ($properties as $property) { + $locations[$property] = $object_type .':'. $object->$idname .':'. $property; + } + + switch ($op) { + case 'update': + foreach ($locations as $property => $location) { + if ($source = db_fetch_object(db_query("SELECT * FROM {locales_source} WHERE location = '%s' AND textgroup = '%s'", $location, $group))) { + // Update entry only if source string changed. + if ($source->source != $object->$property) { + db_query("UPDATE {locales_source} SET source = '%s' WHERE location = '%s' AND textgroup = '%s'", $object->$property, $location, $group); + } + } else { + // Create new entry. + db_query("INSERT INTO {locales_source} (source, location, textgroup) VALUES('%s', '%s', '%s')", $object->$property, $location, $group); + } + } + break; + case 'delete': + foreach ($locations as $location) { + if ($lid = db_result(db_query("SELECT lid FROM {locales_source} WHERE location = '%s' AND textgroup = '%s'", $location, $group))) { + db_query("DELETE FROM {locales_source} WHERE lid = %d", $lid); + db_query("DELETE FROM {locales_target} WHERE lid = %d", $lid); + } + } + break; + } +} /** * Returns a language name