diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php new file mode 100644 index 0000000000..98f9ee4899 --- /dev/null +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php @@ -0,0 +1,249 @@ +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(); + + // 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(); + $product->addVariation($variation); + } + + $product->save(); + $this->product = Product::load($product->id()); + + // Create a translation for each variation on the product. + foreach ($this->product->getVariations() as $variation) { + $variation->addTranslation('fr')->save(); + } + + $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(); + drupal_flush_all_caches(); + + $this->drupalGet($this->product->getTranslation('fr')->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(); + drupal_flush_all_caches(); + + $this->drupalGet($this->product->getTranslation('fr')->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/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index cca824dbe1..b86b802e77 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/Plugin/Field/FieldFormatter/AddToCartFormatter.php b/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php index 8c8fee6f80..deb71c0fb7 100644 --- a/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php +++ b/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php @@ -70,6 +70,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $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 b997494242..2dc1592c14 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\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; @@ -55,11 +56,13 @@ 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) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_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, $entity_repository); $this->attributeFieldManager = $attribute_field_manager; $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); @@ -76,6 +79,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['settings'], $configuration['third_party_settings'], $container->get('entity_type.manager'), + $container->get('entity.repository'), $container->get('commerce_product.attribute_field_manager') ); } @@ -86,7 +90,7 @@ public static function create(ContainerInterface $container, array $configuratio public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $form_state->get('product'); - $variations = $this->variationStorage->loadEnabled($product); + $variations = $this->loadEnabledVariations($product); if (count($variations) === 0) { // Nothing to purchase, tell the parent form to hide itself. $form_state->set('hide_form', TRUE); @@ -141,6 +145,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen } } } + $element['variation'] = [ '#type' => 'value', '#value' => $selected_variation->id(), @@ -264,10 +269,12 @@ protected function getAttributeInfo(ProductVariationInterface $selected_variatio $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 e6a95d672b..7a431d0181 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php @@ -71,8 +71,7 @@ public function settingsSummary() { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { /** @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 = $this->loadEnabledVariations($product); if (count($variations) === 0) { // Nothing to purchase, tell the parent form to hide itself. $form_state->set('hide_form', TRUE); diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php index 4c9fd84b00..d2a1cbce86 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php @@ -2,9 +2,11 @@ namespace Drupal\commerce_product\Plugin\Field\FieldWidget; +use Drupal\commerce_product\Entity\ProductInterface; use Drupal\commerce_product\Entity\ProductVariation; use Drupal\commerce_product\Event\ProductVariationAjaxChangeEvent; use Drupal\commerce_product\Event\ProductEvents; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\WidgetBase; @@ -29,6 +31,13 @@ abstract class ProductVariationWidgetBase extends WidgetBase implements Containe */ protected $variationStorage; + /** + * The entity repository service. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + /** * Constructs a new ProductVariationWidgetBase object. * @@ -44,10 +53,13 @@ abstract class ProductVariationWidgetBase extends WidgetBase implements Containe * 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. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + $this->entityRepository = $entity_repository; $this->variationStorage = $entity_type_manager->getStorage('commerce_product_variation'); } @@ -61,7 +73,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('entity.repository') ); } @@ -103,6 +116,11 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) $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(); @@ -115,4 +133,22 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) return $response; } + /** + * Gets the enabled variations for the product. + * + * @param \Drupal\commerce_product\Entity\ProductInterface $product + * The product. + * + * @return \Drupal\commerce_product\Entity\ProductVariationInterface[] + * An array of variations. + */ + protected function loadEnabledVariations(ProductInterface $product) { + $langcode = $product->language()->getId(); + $variations = $this->variationStorage->loadEnabled($product); + foreach ($variations as $key => $variation) { + $variations[$key] = $this->entityRepository->getTranslationFromContext($variation, $langcode); + } + return $variations; + } + } diff --git a/modules/product/src/ProductLazyBuilders.php b/modules/product/src/ProductLazyBuilders.php index 1a259124b0..92db20ef3c 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; @@ -25,6 +26,13 @@ class ProductLazyBuilders { */ protected $formBuilder; + /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + /** * Constructs a new CartLazyBuilders object. * @@ -32,10 +40,13 @@ class ProductLazyBuilders { * 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 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, For * 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 [];