=== modified file 'docs/hooks.php'
--- docs/hooks.php	2009-07-21 14:53:39 +0000
+++ docs/hooks.php	2009-07-22 13:32:01 +0000
@@ -235,32 +235,6 @@
 }
 
 /**
- * Format data added to an item in the cart for display.
- *
- * Modules that add data to cart items when they are selected should display it
- * with this hook. The return values from each implementation will be
- * concatenated.
- *
- * @param $item
- *   One of the values of the array returned by uc_cart_get_contents().
- * @return
- *   A formatted string to be displayed in the shopping cart block and on the
- *   cart page.
- */
-function hook_cart_item_description($item) {
-  $rows = array();
-  foreach (_uc_cart_product_get_options($item) as $option) {
-    $rows[] = t('@attribute: @option', array('@attribute' => $option['attribute'], '@option' => $option['name']));
-  }
-
-  if (count($rows)) {
-    $output = theme('item_list', $rows, NULL, 'ul', array('class' => 'product-description'));
-  }
-
-  return $output;
-}
-
-/**
  * Register callbacks for a cart pane.
  *
  * The default cart view page displays a table of the cart contents and a few
@@ -855,6 +829,71 @@
 }
 
 /**
+ * Return a structured array representing the given product's description.
+ *
+ * Modules that add data to cart items when they are selected should display it
+ * with this hook. The return values from each implementation will be
+ * sent through to hook_product_description_alter() implementations and then
+ * all descriptions are rendered using drupal_render().
+ *
+ * @param $product
+ *   Product. Usually one of the values of the array returned by
+ *   uc_cart_get_contents().
+ * @return
+ *   A structured array that can be fed into drupal_render().
+ */
+function hook_product_description($product) {
+  $description = array(
+    'attributes' => array(
+      '#product' => array(
+        '#type' => 'value',
+        '#value' => $product,
+      ),
+      '#theme' => 'uc_product_attributes',
+      '#weight' => 1,
+    ),
+  );
+
+  $desc =& $description['attributes'];
+
+  // Cart version of the product has numeric attribute ID => option ID values
+  // so we need to retrieve the right ones
+  if (empty($product->order_id)) {
+    foreach (_uc_cart_product_get_options($product) as $option) {
+      if (!isset($desc[$option['aid']])) {
+        $desc[$option['aid']]['#attribute_name'] = $option['attribute'];
+        $desc[$option['aid']]['#options'] = array($option['name']);
+      }
+      else {
+        $desc[$option['aid']]['#options'][] = $option['name'];
+      }
+    }
+  }
+  else {
+    foreach ((array)$product->data['attributes'] as $attribute => $option) {
+      $desc[] = array(
+        '#attribute_name' => $attribute,
+        '#options' => $option,
+      );
+    }
+  }
+
+  return $description;
+}
+
+/**
+ * Alters the given product description.
+ *
+ * @param $description
+ *   Description array reference.
+ * @param $product
+ *   The product being described.
+ */
+function hook_product_description_alter(&$description, $product) {
+  $description['attributes']['#weight'] = 2;
+}
+
+/**
  * List node types which should be considered products.
  *
  * Trusts the duck philosophy of object identification: if it walks like a duck,

=== modified file 'uc_attribute/uc_attribute.admin.inc'
--- uc_attribute/uc_attribute.admin.inc	2009-07-21 14:29:21 +0000
+++ uc_attribute/uc_attribute.admin.inc	2009-07-22 13:54:11 +0000
@@ -919,16 +919,36 @@
 /**
  * Returns a themed set of attribute options for use in order displays.
  *
- * @param $product Product
+ * @param $element
+ *   Structured array containing the set of attributes with each element
+ *   having a key of attribute ID and the following keys:
+ *     #attribute_name - Attribute name
+ *     #options - Array of option names
  * @return Themed set of attribute options.
  */
-function theme_uc_order_attributes(&$product) {
+function theme_uc_product_attributes(&$element) {
   $option_rows = array();
-  foreach ((array)$product->data['attributes'] as $attribute => $option) {
-    $option_rows[] = t('@attribute: @option', array('@attribute' => $attribute, '@option' => implode(', ', (array)$option)));
-  }
-
-  return theme('item_list', $option_rows, NULL, 'ul', array('class' => 'product-description'));
+
+  foreach (element_children($element) as $key) {
+    $optionstr = '';
+
+    foreach ($element[$key]['#options'] as $option) {
+      // We only need to allow translation from the second option onward
+      if (empty($optionstr)) {
+        $optionstr .= $option;
+      }
+      else {
+        $optionstr .= t(', !option', array('!option' => $option));
+      }
+    }
+    $option_rows[$key] = t('@attribute: @option', array('@attribute' => $element[$key]['#attribute_name'], '@option' => $optionstr));
+  }
+
+  if (!empty($option_rows)) {
+    return theme('item_list', $option_rows, NULL, 'ul', array('class' => 'product-description'));
+  }
+
+  return '';
 }
 
 /**

=== modified file 'uc_attribute/uc_attribute.module'
--- uc_attribute/uc_attribute.module	2009-07-21 14:29:21 +0000
+++ uc_attribute/uc_attribute.module	2009-07-22 13:20:02 +0000
@@ -265,7 +265,7 @@
       'arguments' => array('form' => NULL),
       'file' => 'uc_attribute.admin.inc',
     ),
-    'uc_order_attributes' => array(
+    'uc_product_attributes' => array(
       'arguments' => array('product' => NULL),
       'file' => 'uc_attribute.admin.inc',
     ),
@@ -461,24 +461,45 @@
 }
 
 /**
- * Implementation of hook_cart_item_description().
+ * Implementation of hook_product_description().
  */
-function uc_attribute_cart_item_description($item) {
-  $rows = array();
-  foreach (_uc_cart_product_get_options($item) as $option) {
-    if (!isset($rows[$option['aid']])) {
-      $rows[$option['aid']] = t('@attribute: @option', array('@attribute' => $option['attribute'], '@option' => $option['name']));
-    }
-    else {
-      $rows[$option['aid']] .= t(', @option', array('@option' => $option['name']));
-    }
-  }
-
-  if (count($rows)) {
-    $output = theme('item_list', $rows, NULL, 'ul', array('class' => 'product-description'));
-  }
-
-  return $output;
+function uc_attribute_product_description($product) {
+  $description = array(
+    'attributes' => array(
+      '#product' => array(
+        '#type' => 'value',
+        '#value' => $product,
+      ),
+      '#theme' => 'uc_product_attributes',
+      '#weight' => 1,
+    ),
+  );
+
+  $desc =& $description['attributes'];
+
+  // Cart version of the product has numeric attribute => option values so we
+  // need to retrieve the right ones
+  if (empty($product->order_id)) {
+    foreach (_uc_cart_product_get_options($product) as $option) {
+      if (!isset($desc[$option['aid']])) {
+        $desc[$option['aid']]['#attribute_name'] = $option['attribute'];
+        $desc[$option['aid']]['#options'] = array($option['name']);
+      }
+      else {
+        $desc[$option['aid']]['#options'][] = $option['name'];
+      }
+    }
+  }
+  else {
+    foreach ((array)$product->data['attributes'] as $attribute => $option) {
+      $desc[] = array(
+        '#attribute_name' => $attribute,
+        '#options' => $option,
+      );
+    }
+  }
+
+  return $description;
 }
 
 /******************************************************************************

=== modified file 'uc_cart/uc_cart_checkout_pane.inc'
--- uc_cart/uc_cart_checkout_pane.inc	2009-07-21 14:29:21 +0000
+++ uc_cart/uc_cart_checkout_pane.inc	2009-07-22 13:26:06 +0000
@@ -33,10 +33,7 @@
         'extras' => array(),
       );
       foreach ($items as $item) {
-        $desc = check_plain($item->title);
-        foreach (module_implements('cart_item_description') as $module) {
-          $desc .= module_invoke($module, 'cart_item_description', $item);
-        }
+        $desc = check_plain($item->title) . uc_product_get_description($item);
 
         $price_info = array(
           'price' => $item->price,
@@ -601,10 +598,7 @@
     $total = uc_price($price_info, $context);
     $subtotal += $total;
 
-    $description = check_plain($item->title);
-    foreach (module_implements('cart_item_description') as $module) {
-      $description .= module_invoke($module, 'cart_item_description', $item);
-    }
+    $description = check_plain($item->title) . uc_product_get_description($item);
 
     // Remove node from context to prevent the price from being altered.
     unset($context['subject']);

=== modified file 'uc_order/uc_order.order_pane.inc'
--- uc_order/uc_order.order_pane.inc	2009-07-21 14:29:21 +0000
+++ uc_order/uc_order.order_pane.inc	2009-07-22 13:26:06 +0000
@@ -767,15 +767,8 @@
         '#value' => $product->qty .'&times;',
         '#cell_attributes' => array('align' => 'right'),
       );
-      $options = $product->data['attributes'];
-      $option_rows = array();
-      if (module_exists('uc_attribute') && is_array($options)) {
-        foreach ($options as $attribute => $option) {
-          $option_rows[] = t('@attribute: @options', array('@attribute' => $attribute, '@options' => implode(', ', (array)$option)));
-        }
-      }
       $data['product'] = array(
-        '#value' => check_plain($product->title) . theme('item_list', $option_rows, NULL, 'ul', array('class' => 'product-description')),
+        '#value' => check_plain($product->title) . uc_product_get_description($product),
       );
       $data['model'] = array(
         '#value' => check_plain($product->model),
@@ -887,7 +880,7 @@
         '#cell_attributes' => array('align' => 'right'),
       );
       $data['product'] = array(
-        '#value' => check_plain($product->title) . theme('uc_order_attributes', $product),
+        '#value' => check_plain($product->title) . uc_product_get_description($product),
       );
       $data['model'] = array(
         '#value' => check_plain($product->model),

=== modified file 'uc_product/uc_product.module'
--- uc_product/uc_product.module	2009-07-22 13:16:26 +0000
+++ uc_product/uc_product.module	2009-07-22 13:27:13 +0000
@@ -1176,11 +1176,7 @@
     '#maxlength' => 6
   );
 
-  $description = '';
-  foreach (module_implements('cart_item_description') as $module) {
-    $description .= module_invoke($module, 'cart_item_description', $item);
-  }
-  if ($description) {
+  if ($description = uc_product_get_description($item)) {
     $element['description'] = array('#value' => $description);
   }
 
@@ -1780,6 +1776,32 @@
 }
 
 /**
+ * Return HTML for the product description.
+ *
+ * Modules adding information use hook_product_description() and modules
+ * wanting to alter the output before rendering can do so by implementing
+ * hook_product_description_alter(). By default, all descriptions supplied by
+ * modules via hook_product_description() are concatenated together.
+ *
+ * NOTE: hook_product_description() supercedes the deprecated
+ * hook_cart_item_description().
+ *
+ * @param $product
+ *   Product
+ * @return
+ *   HTML rendered product description.
+ */
+function uc_product_get_description($product) {
+  // Run through implementations of hook_product_description()
+  $description = module_invoke_all('product_description', $product);
+
+  // Now allow alterations via hook_product_description_alter()
+  drupal_alter('product_description', $description, $product);
+
+  return drupal_render($description);
+}
+
+/**
  * Load a product class.
  */
 function uc_product_class_load($class_id) {

=== modified file 'uc_product_kit/uc_product_kit.module'
--- uc_product_kit/uc_product_kit.module	2009-07-01 15:58:50 +0000
+++ uc_product_kit/uc_product_kit.module	2009-07-22 13:26:06 +0000
@@ -926,10 +926,8 @@
       $elements[$unique_id] = $element;
     }
     // Add product specific information
-    $extra = '';
-    foreach (module_implements('cart_item_description') as $module) {
-      $extra .= module_invoke($module, 'cart_item_description', $item);
-    }
+    $extra = uc_product_get_description($item);
+
     if (node_access('view', node_load($item->nid))) {
       $title = l($item->title, 'node/'. $item->nid);
     }

