diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php new file mode 100644 index 00000000..3b137357 --- /dev/null +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php @@ -0,0 +1,244 @@ +setupMultilingual(); + + /** @var \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type */ + $variation_type = ProductVariationType::load($this->variation->bundle()); + $color_attributes = $this->createAttributeSet($variation_type, 'color', [ + 'red' => 'Red', + 'blue' => 'Blue', + ]); + + foreach ($color_attributes as $key => $color_attribute) { + $color_attribute->addTranslation('fr', [ + 'name' => 'FR ' . $color_attribute->label(), + ]); + $color_attribute->save(); + } + $size_attributes = $this->createAttributeSet($variation_type, 'size', [ + 'small' => 'Small', + 'medium' => 'Medium', + 'large' => 'Large', + ]); + foreach ($size_attributes as $key => $size_attribute) { + $size_attribute->addTranslation('fr', [ + 'name' => 'FR ' . $size_attribute->label(), + ]); + $size_attribute->save(); + } + + // Reload the variation since we have new fields. + $this->variation = ProductVariation::load($this->variation->id()); + + // Translate the product's title. + $product = $this->variation->getProduct(); + $product->setTitle('My Super Product'); + $product->addTranslation('fr', [ + 'title' => 'Mon super produit', + ]); + $product->save(); + + // Update first variation to have the attribute's value. + $this->variation->get('attribute_color')->setValue($color_attributes['red']); + $this->variation->get('attribute_size')->setValue($size_attributes['small']); + $this->variation->save(); + $this->variation->addTranslation('fr')->save(); + + // The matrix is intentionally uneven, blue / large is missing. + $attribute_values_matrix = [ + ['red', 'small'], + ['red', 'medium'], + ['red', 'large'], + ['blue', 'small'], + ['blue', 'medium'], + ]; + + // Generate variations off of the attributes values matrix. + foreach ($attribute_values_matrix as $key => $value) { + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = $this->createEntity('commerce_product_variation', [ + 'type' => $variation_type->id(), + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + ]); + $variation->get('attribute_color')->setValue($color_attributes[$value[0]]); + $variation->get('attribute_size')->setValue($size_attributes[$value[1]]); + $variation->save(); + $variation->addTranslation('fr')->save(); + $product->addVariation($variation); + } + + $product->save(); + $this->product = Product::load($product->id()); + + $this->variations = $product->getVariations(); + $this->colorAttributes = $color_attributes; + $this->sizeAttributes = $size_attributes; + } + + /** + * Sets up the multilingual items. + */ + protected function setupMultilingual() { + // Add a new language. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Enable translation for the product and ensure the change is picked up. + $this->container->get('content_translation.manager')->setEnabled('commerce_product', $this->variation->bundle(), TRUE); + $this->container->get('content_translation.manager')->setEnabled('commerce_product_variation', $this->variation->bundle(), TRUE); + $this->container->get('entity.manager')->clearCachedDefinitions(); + $this->container->get('router.builder')->rebuild(); + $this->container->get('entity.definition_update_manager')->applyUpdates(); + + // Rebuild the container so that the new languages are picked up by services + // that hold a list of languages. + $this->rebuildContainer(); + } + + /** + * Tests that the attribute widget uses translated items. + */ + public function testProductVariationAttributesWidget() { + $this->drupalGet($this->product->toUrl()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'Red'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'Small'); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_color]', $this->colorAttributes['blue']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['medium']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['large']->id()); + $this->getSession()->getPage()->pressButton('Add to cart'); + + // Change the site language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + + $this->drupalGet($this->product->toUrl()); + // Use AJAX to change the size to Medium, keeping the color on Red. + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'FR Red'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_color]', $this->colorAttributes['blue']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['small']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['large']->id()); + + // Use AJAX to change the color to Blue, keeping the size on Medium. + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][attributes][attribute_color]', 'FR Blue'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'FR Blue'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_color]', $this->colorAttributes['red']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['small']->id()); + $this->assertAttributeDoesNotExist('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['large']->id()); + $this->getSession()->getPage()->pressButton('Add to cart'); + + $this->cart = Order::load($this->cart->id()); + $order_items = $this->cart->getItems(); + $this->assertOrderItemInOrder($this->variations[0]->getTranslation('fr'), $order_items[0]); + $this->assertOrderItemInOrder($this->variations[5]->getTranslation('fr'), $order_items[1]); + } + + /** + * Tests the title widget has translated variation title. + */ + public function testProductVariationTitleWidget() { + $order_item_form_display = EntityFormDisplay::load('commerce_order_item.default.add_to_cart'); + $order_item_form_display->setComponent('purchased_entity', [ + 'type' => 'commerce_product_variation_title', + ]); + $order_item_form_display->save(); + + $this->drupalGet($this->product->toUrl()); + $this->assertSession()->selectExists('purchased_entity[0][variation]'); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'My Super Product - Red, Small'); + $this->getSession()->getPage()->pressButton('Add to cart'); + + // Change the site language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + $this->product = Product::load($this->product->id()); + $this->drupalGet($this->product->toUrl()); + // Use AJAX to change the size to Medium, keeping the color on Red. + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Red, FR Small'); + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][variation]', 'Mon super produit - FR Red, FR Medium'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Red, FR Medium'); + $this->assertSession()->pageTextContains('Mon super produit - FR Red, FR Medium'); + // Use AJAX to change the color to Blue, keeping the size on Medium. + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][variation]', 'Mon super produit - FR Blue, FR Medium'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Blue, FR Medium'); + $this->assertSession()->pageTextContains('Mon super produit - FR Blue, FR Medium'); + $this->getSession()->getPage()->pressButton('Add to cart'); + + $this->cart = Order::load($this->cart->id()); + $order_items = $this->cart->getItems(); + $this->assertOrderItemInOrder($this->variations[0]->getTranslation('fr'), $order_items[0]); + $this->assertOrderItemInOrder($this->variations[5]->getTranslation('fr'), $order_items[1]); + } + +} diff --git a/modules/checkout/tests/src/Functional/CheckoutOrderTest.php b/modules/checkout/tests/src/Functional/CheckoutOrderTest.php index 7ed0ccbc..13e10c58 100644 --- a/modules/checkout/tests/src/Functional/CheckoutOrderTest.php +++ b/modules/checkout/tests/src/Functional/CheckoutOrderTest.php @@ -135,7 +135,7 @@ class CheckoutOrderTest extends CommerceBrowserTestBase { */ public function testGuestOrderCheckout() { $this->drupalLogout(); - $this->drupalGet($this->product->toUrl()->toString()); + $this->drupalGet($this->product->toUrl()); $this->submitForm([], 'Add to cart'); $this->assertSession()->pageTextContains('1 item'); $cart_link = $this->getSession()->getPage()->findLink('your cart'); @@ -165,7 +165,7 @@ class CheckoutOrderTest extends CommerceBrowserTestBase { $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.'); $this->assertSession()->pageTextContains('0 items'); // Test second order. - $this->drupalGet($this->product->toUrl()->toString()); + $this->drupalGet($this->product->toUrl()); $this->submitForm([], 'Add to cart'); $this->assertSession()->pageTextContains('1 item'); $cart_link = $this->getSession()->getPage()->findLink('your cart'); @@ -212,7 +212,7 @@ class CheckoutOrderTest extends CommerceBrowserTestBase { $config->save(); $this->drupalLogout(); - $this->drupalGet($this->product->toUrl()->toString()); + $this->drupalGet($this->product->toUrl()); $this->submitForm([], 'Add to cart'); $cart_link = $this->getSession()->getPage()->findLink('your cart'); $cart_link->click(); @@ -325,7 +325,7 @@ class CheckoutOrderTest extends CommerceBrowserTestBase { 'stores' => [$this->store], ]); // Adding a new product to the cart resets the checkout step. - $this->drupalGet($product2->toUrl()->toString()); + $this->drupalGet($product2->toUrl()); $this->submitForm([], 'Add to cart'); $this->getSession()->getPage()->findLink('your cart')->click(); $this->submitForm([], 'Checkout'); diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index 94ca3ecc..13cf999c 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -2,9 +2,9 @@ namespace Drupal\commerce_order\Entity; +use Drupal\commerce\Entity\CommerceContentEntityBase; use Drupal\commerce_order\Adjustment; use Drupal\commerce_store\Entity\StoreInterface; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -68,7 +68,7 @@ use Drupal\profile\Entity\ProfileInterface; * field_ui_base_route = "entity.commerce_order_type.edit_form" * ) */ -class Order extends ContentEntityBase implements OrderInterface { +class Order extends CommerceContentEntityBase implements OrderInterface { use EntityChangedTrait; @@ -91,7 +91,8 @@ class Order extends ContentEntityBase implements OrderInterface { * {@inheritdoc} */ public function getStore() { - return $this->get('store_id')->entity; + $store = $this->getTranslatedReferencedEntities('store_id'); + return reset($store); } /** diff --git a/modules/order/src/Entity/OrderItem.php b/modules/order/src/Entity/OrderItem.php index d89faf14..d4d7ea77 100644 --- a/modules/order/src/Entity/OrderItem.php +++ b/modules/order/src/Entity/OrderItem.php @@ -2,9 +2,9 @@ namespace Drupal\commerce_order\Entity; +use Drupal\commerce\Entity\CommerceContentEntityBase; use Drupal\commerce_order\Adjustment; use Drupal\commerce_price\Price; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -46,7 +46,7 @@ use Drupal\Core\Field\BaseFieldDefinition; * field_ui_base_route = "entity.commerce_order_item_type.edit_form", * ) */ -class OrderItem extends ContentEntityBase implements OrderItemInterface { +class OrderItem extends CommerceContentEntityBase implements OrderItemInterface { use EntityChangedTrait; @@ -75,7 +75,8 @@ class OrderItem extends ContentEntityBase implements OrderItemInterface { * {@inheritdoc} */ public function getPurchasedEntity() { - return $this->get('purchased_entity')->entity; + $purchased_entity = $this->getTranslatedReferencedEntities('purchased_entity'); + return reset($purchased_entity); } /** diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index cca824db..b86b802e 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -5,7 +5,7 @@ services: commerce_product.lazy_builders: class: Drupal\commerce_product\ProductLazyBuilders - arguments: ['@entity_type.manager', '@form_builder'] + arguments: ['@entity_type.manager', '@form_builder', '@entity.repository'] commerce_product.variation_field_renderer: class: Drupal\commerce_product\ProductVariationFieldRenderer diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index b545fd1c..439a9663 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -2,8 +2,8 @@ namespace Drupal\commerce_product\Entity; +use Drupal\commerce\Entity\CommerceContentEntityBase; use Drupal\Core\Entity\EntityPublishedTrait; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -71,7 +71,7 @@ use Drupal\user\UserInterface; * field_ui_base_route = "entity.commerce_product_type.edit_form", * ) */ -class Product extends ContentEntityBase implements ProductInterface { +class Product extends CommerceContentEntityBase implements ProductInterface { use EntityChangedTrait; use EntityPublishedTrait; @@ -110,8 +110,7 @@ class Product extends ContentEntityBase implements ProductInterface { * {@inheritdoc} */ public function getStores() { - $stores = $this->get('stores')->referencedEntities(); - return $this->ensureTranslations($stores); + return $this->getTranslatedReferencedEntities('stores'); } /** @@ -186,8 +185,7 @@ class Product extends ContentEntityBase implements ProductInterface { * {@inheritdoc} */ public function getVariations() { - $variations = $this->get('variations')->referencedEntities(); - return $this->ensureTranslations($variations); + return $this->getTranslatedReferencedEntities('variations'); } /** @@ -253,33 +251,12 @@ class Product extends ContentEntityBase implements ProductInterface { foreach ($this->getVariations() as $variation) { // Return the first active variation. if ($variation->isActive() && $variation->access('view')) { - return $variation; + return $variation->getTranslation($this->language()->getId()); } } } /** - * Ensures that the provided entities are in the current entity's language. - * - * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities - * The entities to process. - * - * @return \Drupal\Core\Entity\ContentEntityInterface[] - * The processed entities. - */ - protected function ensureTranslations(array $entities) { - $langcode = $this->language()->getId(); - foreach ($entities as $index => $entity) { - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - if ($entity->hasTranslation($langcode)) { - $entities[$index] = $entity->getTranslation($langcode); - } - } - - return $entities; - } - - /** * {@inheritdoc} */ public function preSave(EntityStorageInterface $storage) { diff --git a/modules/product/src/Entity/ProductVariation.php b/modules/product/src/Entity/ProductVariation.php index 052607be..78d943d4 100644 --- a/modules/product/src/Entity/ProductVariation.php +++ b/modules/product/src/Entity/ProductVariation.php @@ -2,10 +2,10 @@ namespace Drupal\commerce_product\Entity; +use Drupal\commerce\Entity\CommerceContentEntityBase; use Drupal\commerce\EntityHelper; use Drupal\commerce_price\Price; use Drupal\Core\Cache\Cache; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -57,7 +57,7 @@ use Drupal\user\UserInterface; * field_ui_base_route = "entity.commerce_product_variation_type.edit_form", * ) */ -class ProductVariation extends ContentEntityBase implements ProductVariationInterface { +class ProductVariation extends CommerceContentEntityBase implements ProductVariationInterface { use EntityChangedTrait; @@ -91,7 +91,7 @@ class ProductVariation extends ContentEntityBase implements ProductVariationInte * {@inheritdoc} */ public function getProduct() { - return $this->get('product_id')->entity; + return $this->getTranslatedReferencedEntity('product_id'); } /** @@ -288,7 +288,10 @@ class ProductVariation extends ContentEntityBase implements ProductVariationInte foreach ($this->getAttributeFieldNames() as $field_name) { $field = $this->get($field_name); if (!$field->isEmpty()) { - $attribute_values[$field_name] = $field->entity; + /** @var \Drupal\commerce_product\Entity\ProductAttributeValueInterface $entity */ + $entity = $field->entity; + $attribute_value = $entity->getTranslation($this->language()->getId()); + $attribute_values[$field_name] = $attribute_value; } } @@ -306,7 +309,9 @@ class ProductVariation extends ContentEntityBase implements ProductVariationInte $attribute_value = NULL; $field = $this->get($field_name); if (!$field->isEmpty()) { - $attribute_value = $field->entity; + /** @var \Drupal\commerce_product\Entity\ProductAttributeValueInterface $entity */ + $entity = $field->entity; + $attribute_value = $entity->getTranslation($this->language()->getId()); } return $attribute_value; @@ -362,7 +367,6 @@ class ProductVariation extends ContentEntityBase implements ProductVariationInte // When there are no attribute fields, there's only one variation. $title = $product_title; } - return $title; } diff --git a/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php b/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php index 8c8fee6f..deb71c0f 100644 --- a/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php +++ b/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php @@ -70,6 +70,7 @@ class AddToCartFormatter extends FormatterBase { $items->getEntity()->id(), $this->viewMode, $this->getSetting('combine'), + $langcode, ], ], '#create_placeholder' => TRUE, diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index b9974942..5c2cc346 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -6,6 +6,7 @@ use Drupal\commerce_product\Entity\ProductVariationInterface; use Drupal\commerce_product\ProductAttributeFieldManagerInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; @@ -27,6 +28,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class ProductVariationAttributesWidget extends ProductVariationWidgetBase implements ContainerFactoryPluginInterface { /** + * The entity repository service. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** * The attribute field manager. * * @var \Drupal\commerce_product\ProductAttributeFieldManagerInterface @@ -55,12 +63,15 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem * Any third party settings. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager * The attribute field manager. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, ProductAttributeFieldManagerInterface $attribute_field_manager) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager); + $this->entityRepository = $entity_repository; $this->attributeFieldManager = $attribute_field_manager; $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); } @@ -76,6 +87,7 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem $configuration['settings'], $configuration['third_party_settings'], $container->get('entity_type.manager'), + $container->get('entity.repository'), $container->get('commerce_product.attribute_field_manager') ); } @@ -141,6 +153,7 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem } } } + $element['variation'] = [ '#type' => 'value', '#value' => $selected_variation->id(), @@ -264,10 +277,12 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem $field = $field_definitions[$field_name]; /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ $attribute = $this->attributeStorage->load($attribute_ids[$index]); + // Make sure we have translation for attribute. + $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); $attributes[$field_name] = [ 'field_name' => $field_name, - 'title' => $field->getLabel(), + 'title' => $attribute->label(), 'required' => $field->isRequired(), 'element_type' => $attribute->getElementType(), ]; diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php index e6a95d67..682fed72 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php @@ -72,7 +72,10 @@ class ProductVariationTitleWidget extends ProductVariationWidgetBase implements /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $form_state->get('product'); /** @var \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations */ - $variations = $this->variationStorage->loadEnabled($product); + $variations = []; + foreach ($product->getVariations() as $variation) { + $variations[] = $variation; + } if (count($variations) === 0) { // Nothing to purchase, tell the parent form to hide itself. $form_state->set('hide_form', TRUE); @@ -152,8 +155,8 @@ class ProductVariationTitleWidget extends ProductVariationWidgetBase implements * The selected variation. */ protected function selectVariationFromUserInput(array $variations, array $user_input) { - $current_variation = NULL; - if (!empty($user_input['variation']) && $variations[$user_input['variation']]) { + $current_variation = reset($variations); + if (!empty($user_input) && !empty($user_input['variation']) && $variations[$user_input['variation']]) { $current_variation = $variations[$user_input['variation']]; } diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php index 4c9fd84b..1b8115c1 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php @@ -103,6 +103,11 @@ abstract class ProductVariationWidgetBase extends WidgetBase implements Containe $response = $ajax_renderer->renderResponse($form, $request, $route_match); $variation = ProductVariation::load($form_state->get('selected_variation')); + /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ + $product = $form_state->get('product'); + if ($variation->hasTranslation($product->language()->getId())) { + $variation = $variation->getTranslation($product->language()->getId()); + } /** @var \Drupal\commerce_product\ProductVariationFieldRendererInterface $variation_field_renderer */ $variation_field_renderer = \Drupal::service('commerce_product.variation_field_renderer'); $view_mode = $form_state->get('form_display')->getMode(); diff --git a/modules/product/src/ProductLazyBuilders.php b/modules/product/src/ProductLazyBuilders.php index 1a259124..92db20ef 100644 --- a/modules/product/src/ProductLazyBuilders.php +++ b/modules/product/src/ProductLazyBuilders.php @@ -2,6 +2,7 @@ namespace Drupal\commerce_product; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormState; @@ -26,16 +27,26 @@ class ProductLazyBuilders { protected $formBuilder; /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** * Constructs a new CartLazyBuilders object. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityRepositoryInterface $entity_repository) { $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; + $this->entityRepository = $entity_repository; } /** @@ -47,15 +58,20 @@ class ProductLazyBuilders { * The view mode used to render the product. * @param bool $combine * TRUE to combine order items containing the same product variation. + * @param string $langcode + * The langcode for the language that should be used in form. * * @return array * A renderable array containing the cart form. */ - public function addToCartForm($product_id, $view_mode, $combine) { + public function addToCartForm($product_id, $view_mode, $combine, $langcode) { /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */ $order_item_storage = $this->entityTypeManager->getStorage('commerce_order_item'); /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $this->entityTypeManager->getStorage('commerce_product')->load($product_id); + // Load Product for current language. + $product = $this->entityRepository->getTranslationFromContext($product, $langcode); + $default_variation = $product->getDefaultVariation(); if (!$default_variation) { return []; diff --git a/modules/product/src/ProductVariationStorage.php b/modules/product/src/ProductVariationStorage.php index 538b5b23..fc830b2f 100644 --- a/modules/product/src/ProductVariationStorage.php +++ b/modules/product/src/ProductVariationStorage.php @@ -9,6 +9,7 @@ use Drupal\commerce_product\Event\ProductEvents; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Language\LanguageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -28,6 +29,13 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr protected $requestStack; /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** * Constructs a new ProductVariationStorage object. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type @@ -44,11 +52,14 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr * The event dispatcher. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The request stack. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. */ - public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack) { + public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, EntityRepositoryInterface $entity_repository) { parent::__construct($entity_type, $database, $entity_manager, $cache, $language_manager, $event_dispatcher); $this->requestStack = $request_stack; + $this->entityRepository = $entity_repository; } /** @@ -62,7 +73,8 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr $container->get('cache.entity'), $container->get('language_manager'), $container->get('event_dispatcher'), - $container->get('request_stack') + $container->get('request_stack'), + $container->get('entity.repository') ); } @@ -73,7 +85,7 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr $variations = $this->loadByProperties(['sku' => $sku]); $variation = reset($variations); - return $variation ?: NULL; + return $variation ? $this->entityRepository->getTranslationFromContext($variation) : NULL; } /** @@ -81,12 +93,13 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr */ public function loadFromContext(ProductInterface $product) { $current_request = $this->requestStack->getCurrentRequest(); + $langcode = $product->language()->getId(); if ($variation_id = $current_request->query->get('v')) { if (in_array($variation_id, $product->getVariationIds())) { /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ $variation = $this->load($variation_id); if ($variation->isActive() && $variation->access('view')) { - return $variation; + return $this->entityRepository->getTranslationFromContext($variation, $langcode); } } } @@ -98,8 +111,8 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr */ public function loadEnabled(ProductInterface $product) { $ids = []; - foreach ($product->variations as $variation) { - $ids[$variation->target_id] = $variation->target_id; + foreach ($product->getVariationIds() as $variation_id) { + $ids[$variation_id] = $variation_id; } // Speed up loading by filtering out the IDs of disabled variations. $query = $this->getQuery() @@ -115,6 +128,14 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $enabled_variation */ $enabled_variations = $this->loadMultiple($result); + + if ($product->isTranslatable()) { + $langcode = $product->language()->getId(); + foreach ($enabled_variations as $index => $enabled_variation) { + $enabled_variations[$index] = $this->entityRepository->getTranslationFromContext($enabled_variation, $langcode); + } + } + // Allow modules to apply own filtering (based on date, stock, etc). $event = new FilterVariationsEvent($product, $enabled_variations); $this->eventDispatcher->dispatch(ProductEvents::FILTER_VARIATIONS, $event); diff --git a/modules/product/tests/src/Kernel/ProductVariationGeneratedTitleTest.php b/modules/product/tests/src/Kernel/ProductVariationGeneratedTitleTest.php new file mode 100644 index 00000000..ecc6f3e7 --- /dev/null +++ b/modules/product/tests/src/Kernel/ProductVariationGeneratedTitleTest.php @@ -0,0 +1,194 @@ +installEntitySchema('commerce_product_variation'); + $this->installEntitySchema('commerce_product'); + $this->installEntitySchema('commerce_product_attribute'); + $this->installEntitySchema('commerce_product_attribute_value'); + $this->installConfig(['commerce_product']); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $variation_type = ProductVariationType::create([ + 'id' => 'generate_title', + 'label' => 'Generate title test', + 'orderItemType' => 'default', + 'generateTitle' => TRUE, + ]); + $variation_type->save(); + $this->variationType = $variation_type; + + $product_type = ProductType::create([ + 'id' => 'generate_title', + 'label' => 'Generate title test', + 'variationType' => $variation_type->id(), + ]); + $product_type->save(); + commerce_product_add_stores_field($product_type); + commerce_product_add_variations_field($product_type); + $this->productType = $product_type; + + $color_attribute = ProductAttribute::create([ + 'id' => 'color', + 'label' => 'Color', + ]); + $color_attribute->save(); + $this->container + ->get('commerce_product.attribute_field_manager') + ->createField($color_attribute, $this->variationType->id()); + $this->attribute = $color_attribute; + } + + /** + * Tests the title is generated. + */ + public function testTitleGenerated() { + // Variations without a product have no title, because it can not be + // determined. + $variation = ProductVariation::create([ + 'type' => $this->variationType->id(), + ]); + $variation->save(); + $this->assertNull($variation->label()); + + // When variations have a product, but no attributes, the variation label + // should be the product's. + $product = Product::create([ + 'type' => $this->productType->id(), + 'title' => 'My Super Product', + 'variations' => [$variation], + ]); + $product->save(); + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = $this->reloadEntity($variation); + $this->assertEquals($variation->label(), $product->label()); + + // With attribute values, the variation title should be the product plus all + // of its attributes. + $color_black = ProductAttributeValue::create([ + 'attribute' => $this->attribute->id(), + 'name' => 'Black', + 'weight' => 3, + ]); + $color_black->save(); + + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = $this->reloadEntity($variation); + $variation->get('attribute_color')->setValue($color_black); + $variation->save(); + + $this->assertNotEquals($variation->label(), $product->label()); + $this->assertEquals($variation->label(), sprintf('%s - %s', $product->label(), $color_black->label())); + } + + /** + * Tests that creating a new variation creates a translated title. + */ + public function testMultilingualTitle() { + $this->container->get('content_translation.manager') + ->setEnabled('commerce_product_variation', $this->variationType->id(), TRUE); + $this->container->get('content_translation.manager') + ->setEnabled('commerce_product', $this->productType->id(), TRUE); + $this->container->get('content_translation.manager') + ->setEnabled('commerce_product_attribute_value', $this->attribute->id(), TRUE); + + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = ProductVariation::create([ + 'type' => $this->variationType->id(), + ]); + $variation->save(); + $this->assertNull($variation->label()); + $product = Product::create([ + 'type' => $this->productType->id(), + 'title' => 'My Super Product', + 'variations' => [$variation], + ]); + $product->addTranslation('fr', [ + 'title' => 'Mon super produit', + ]); + $product->save(); + + // Generating a translation of the variation should create a title which + // has the product's translated title. + $translation = $variation->addTranslation('fr', []); + $translation->save(); + + $this->assertEquals($product->getTranslation('fr')->label(), $variation->getTranslation('fr')->label()); + + // Verify translated attributes are used in the generated title. + $color_black = ProductAttributeValue::create([ + 'attribute' => $this->attribute->id(), + 'name' => 'Black', + 'weight' => 3, + ]); + $color_black->save(); + $color_black->addTranslation('fr', [ + 'name' => 'Noir', + ]); + $color_black->save(); + + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = $this->reloadEntity($variation); + $variation->get('attribute_color')->setValue($color_black); + $variation->save(); + $variation->getTranslation('fr')->save(); + $this->assertEquals($variation->getTranslation('fr')->label(), sprintf('%s - %s', $product->getTranslation('fr')->label(), $color_black->getTranslation('fr')->label())); + } + +} diff --git a/modules/product/tests/src/Kernel/ProductVariationStorageMultilingualTest.php b/modules/product/tests/src/Kernel/ProductVariationStorageMultilingualTest.php new file mode 100644 index 00000000..cb488fe1 --- /dev/null +++ b/modules/product/tests/src/Kernel/ProductVariationStorageMultilingualTest.php @@ -0,0 +1,186 @@ +installEntitySchema('commerce_product_variation'); + $this->installEntitySchema('commerce_product'); + $this->installConfig(['commerce_product']); + + $this->variationStorage = $this->container->get('entity_type.manager')->getStorage('commerce_product_variation'); + $this->languageManager = $this->container->get('language_manager'); + $this->languageDefault = $this->container->get('language.default'); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + ConfigurableLanguage::createFromLangcode('sr')->save(); + ConfigurableLanguage::createFromLangcode('de')->save(); + + $variation_type = ProductVariationType::create([ + 'id' => 'multilingual', + 'label' => 'Multilingual', + 'orderItemType' => 'default', + 'generateTitle' => FALSE, + ]); + $variation_type->save(); + $this->container->get('content_translation.manager') + ->setEnabled('commerce_product_variation', $variation_type->id(), TRUE); + $product_type = ProductType::create([ + 'id' => 'multilingual', + 'label' => 'Multilingual', + 'variationType' => $variation_type->id(), + ]); + $product_type->save(); + commerce_product_add_stores_field($product_type); + commerce_product_add_variations_field($product_type); + $this->container->get('content_translation.manager') + ->setEnabled('commerce_product', $product_type->id(), TRUE); + + $sku = 'STORAGE-MULTILINGUAL-TEST'; + $variation = ProductVariation::create([ + 'type' => $variation_type->id(), + 'sku' => $sku, + 'title' => 'English variation', + ]); + $variation->save(); + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = $this->reloadEntity($variation); + $variation->addTranslation('fr', [ + 'title' => 'Variation française', + ]); + $variation->addTranslation('sr', [ + 'title' => 'Srpska varijacija', + ]); + $variation->save(); + $product = Product::create([ + 'type' => $product_type->id(), + 'variations' => [$variation], + ]); + $product->addTranslation('fr'); + $product->addTranslation('sr'); + $product->save(); + $this->product = $product; + } + + /** + * Tests loading variations by SKU in French. + */ + public function testLoadBySkuFr() { + $this->languageDefault->set($this->languageManager->getLanguage('fr')); + $result = $this->variationStorage->loadBySku($this->testSku); + $this->assertEquals('Variation française', $result->label()); + } + + /** + * Tests loading variations by SKU in Serbian. + */ + public function testLoadBySkuSr() { + $this->languageDefault->set($this->languageManager->getLanguage('sr')); + $result = $this->variationStorage->loadBySku($this->testSku); + $this->assertEquals('Srpska varijacija', $result->label()); + } + + /** + * Tests loading variations by SKU in German (untranslated language.) + */ + public function testLoadBySkuDe() { + $this->languageDefault->set($this->languageManager->getLanguage('de')); + $result = $this->variationStorage->loadBySku($this->testSku); + $this->assertEquals('English variation', $result->label()); + } + + /** + * Tests loadEnabled() method. + */ + public function testLoadEnabled() { + $enabled = $this->variationStorage->loadEnabled($this->product); + $enabled_variation = reset($enabled); + $this->assertEquals($enabled_variation->language()->getId(), 'en'); + + $enabled = $this->variationStorage->loadEnabled($this->product->getTranslation('fr')); + $enabled_variation = reset($enabled); + $this->assertEquals($enabled_variation->language()->getId(), 'fr'); + } + + /** + * Tests loadFromContext() method. + */ + public function testLoadFromContext() { + $product = $this->product->getTranslation('sr'); + $request = Request::create(''); + $request->query->add([ + 'v' => $product->getDefaultVariation()->id(), + ]); + // Push the request to the request stack so `current_route_match` works. + $this->container->get('request_stack')->push($request); + $context_variation = $this->variationStorage->loadFromContext($product); + $this->assertEquals($context_variation->language()->getId(), 'sr'); + } + +} diff --git a/modules/product/tests/src/Kernel/ProductVariationStorageTest.php b/modules/product/tests/src/Kernel/ProductVariationStorageTest.php index d8117641..8a6ac618 100644 --- a/modules/product/tests/src/Kernel/ProductVariationStorageTest.php +++ b/modules/product/tests/src/Kernel/ProductVariationStorageTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\commerce_product\Kernel; use Drupal\commerce_product\Entity\Product; use Drupal\commerce_product\Entity\ProductVariation; use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase; +use Symfony\Component\HttpFoundation\Request; /** * Tests the product variation storage. @@ -94,4 +95,38 @@ class ProductVariationStorageTest extends CommerceKernelTestBase { $this->assertEquals(reset($variations)->getSku(), reset($variationsFiltered)->getSku(), 'The sort order of the variations remains the same'); } + /** + * Tests loadFromContext() method. + */ + public function testLoadFromContext() { + $variations = []; + for ($i = 1; $i <= 3; $i++) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => strtolower($this->randomMachineName()), + 'title' => $this->randomString(), + ]); + $variation->save(); + $variations[] = $variation; + } + $variations = array_reverse($variations); + $product = Product::create([ + 'type' => 'default', + 'variations' => $variations, + ]); + $product->save(); + + $request = Request::create(''); + $request->query->add([ + 'v' => end($variations)->id(), + ]); + // Push the request to the request stack so `current_route_match` works. + $this->container->get('request_stack')->push($request); + + $this->assertNotEquals($request->query->get('v'), $product->getDefaultVariation()->id()); + + $context_variation = $this->variationStorage->loadFromContext($product); + $this->assertEquals($request->query->get('v'), $context_variation->id()); + } + } diff --git a/modules/promotion/src/Entity/Promotion.php b/modules/promotion/src/Entity/Promotion.php index 69af224b..8837a3ff 100644 --- a/modules/promotion/src/Entity/Promotion.php +++ b/modules/promotion/src/Entity/Promotion.php @@ -3,11 +3,11 @@ namespace Drupal\commerce_promotion\Entity; use Drupal\commerce\ConditionGroup; +use Drupal\commerce\Entity\CommerceContentEntityBase; use Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferInterface; use Drupal\Core\Datetime\DrupalDateTime; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -66,7 +66,7 @@ use Drupal\Core\Field\BaseFieldDefinition; * }, * ) */ -class Promotion extends ContentEntityBase implements PromotionInterface { +class Promotion extends CommerceContentEntityBase implements PromotionInterface { /** * {@inheritdoc} @@ -136,7 +136,7 @@ class Promotion extends ContentEntityBase implements PromotionInterface { * {@inheritdoc} */ public function getStores() { - return $this->get('stores')->referencedEntities(); + return $this->getTranslatedReferencedEntities('stores'); } /** diff --git a/src/Entity/CommerceContentEntityBase.php b/src/Entity/CommerceContentEntityBase.php new file mode 100644 index 00000000..85774a18 --- /dev/null +++ b/src/Entity/CommerceContentEntityBase.php @@ -0,0 +1,55 @@ +get($field_name)->referencedEntities(); + return $this->ensureTranslations($referenced_entities); + } + + /** + * {@inheritdoc} + */ + public function getTranslatedReferencedEntity($field_name) { + $referenced_entities = $this->getTranslatedReferencedEntities($field_name); + return reset($referenced_entities); + } + + /** + * Ensures entities are in the current entity's language, if possible. + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * The entities to process. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * The processed entities. + */ + protected function ensureTranslations(array $entities) { + if ($this->isTranslatable()) { + $langcode = $this->language()->getId(); + } + else { + $langcode = $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_INTERFACE)->getId(); + } + foreach ($entities as $index => $entity) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + if ($entity->hasTranslation($langcode)) { + $entities[$index] = $entity->getTranslation($langcode); + } + } + + return $entities; + } + +} diff --git a/src/Entity/CommerceContentEntityInterface.php b/src/Entity/CommerceContentEntityInterface.php new file mode 100644 index 00000000..d776d110 --- /dev/null +++ b/src/Entity/CommerceContentEntityInterface.php @@ -0,0 +1,34 @@ +