Support from Acquia helps fund testing for Drupal Acquia logo

Comments

StryKaizer created an issue. See original summary.

StryKaizer’s picture

Status: Active » Needs review
FileSize
659 bytes
s.messaris’s picture

willeaton’s picture

Status: Needs review » Needs work

This didn't work for me. I don't get any translate option. I really need this for a site which has a Spanish version. The delivery options only appear in English and they are not translatable in any way

StryKaizer’s picture

Status: Needs work » Needs review

@willeaton it is working here.

- Apply patch
- Rebuild cache
- Enable translations for shipping (on /admin/config/regional/content-language)

Then you should be able to browse to /admin/commerce/config/shipping-methods/1/translations
where 1 is the id of the shipping method you wish to translate

willeaton’s picture

Hi, for some reason, anything I tick on the page: /admin/config/regional/content-language doesn't update or get stored, on reload the ticked box gets unticked again.
I cleaned cache and did an entity-updates just in case.
I also tried navigating to admin/commerce/config/shipping-methods/1/translations manually and got a 404.
Could it be you have some patch applied to fix this issue?

Thanks for getting back to me.

willeaton’s picture

OK, figured it out, just in case anyone else has this problem, the page /admin/config/regional/content-language suffers from having a massive form which standard apache config won't allow to be submitted. You have to increase the max_input_vars value. Even after that I had issues with mysql and had to add this line to my settings.php:

$databases['default']['default']['init_commands'] = array(
'isolation' => "SET SESSION tx_isolation='READ-COMMITTED'"
);

I think it might be related to this case and the fact that I have search API installed:
https://www.drupal.org/project/drupal/issues/2833539

TwiiK’s picture

I'm not sure how entity translation is handled normally, but I found this implementation to be unusable in praxis. It feels like I'm duplicating the shipping method for every language and that every duplicate can have completely different settings. When all I wanted was to translate the display label.

My clients create and manage shipping methods themselves and this approach is too unwieldy. I'll just see if I can put the label through a translate function before it's shown in the checkout so that they can translate it using the interface translation for the time being.

mennovdheuvel’s picture

For future visitors: The best place to add a t() that I found was in the commerce_shipping/src/Plugin/Field/FieldFormatter/ShippingMethodFormatter.php file. Just wrap the call to get the label at line 39 in a t() like so:

'#markup' => t($shipping_services[$shipping_service_id]->getLabel()),

trebormc’s picture

The entity does not allow the translation of the label, apart from the fact that the interface is confusing to know which values are translatable and which are not.

I have made a patch with the function t () in two different places to allow the label to be translated.

Status: Needs review » Needs work
FiNeX’s picture

Hi, will this patch be committed? It is very useful :-) Thanks!

streger’s picture

Tested both patches without happy results.

First one does make the Shipping method entity translatable on the /admin/config/regional/content-language but apparently that works only for Name field and doesn't work for 'Rate label'. I do get the languages config translations in /admin/commerce/config/shipping-methods/x/translations but the 'Rate label' translation on other languages just replaces the original on all languages.

Second one didn't work at all. I was expecting the User interface translation variable, but that didn't happen after rendering it on frontend.

Berdir’s picture

Using t() for user input is a security issue that would allow XSS through that, so #10 will not be committed.

Adding the canonical link template is the correct direction, but I had the problem that this broke the edit form, as I assume the route provider gets confused. And on top of that, yes, the whole rate configuration field would need to be translatable, which means not just the label but all rate-related configuration, which might or might not actually work. I also assume it needs code changes to make sure it loads the correct translation.

There's an alternative, that's not pretty either but might be easier to implement. That would be to expose the current language as a condition, then you can set up different shipping methods for each language. Haven't quite figured out yet how to extend the conditions widget to show that condition plugin, as it seems to be tied to entity types only?

Berdir’s picture

Here's such a language condition plugin, based on the core implementation, main difference is that it uses the injected language manager to get the value instead of context. The entity_type requirement is a bit strange as this doesn't have any requirements, so I just added a dummy and them I'm completely ignoring that.


namespace Drupal\yourmodule\Plugin\Commerce\Condition;

use Drupal\commerce\Plugin\Commerce\Condition\ConditionBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a language commerce condition.
 *
 * @CommerceCondition(
 *   id = "yourmodule_language",
 *   label = @Translation("Language"),
 *   display_label = @Translation("Language"),
 *   category = @Translation("Language"),
 *   entity_type = "commerce_order",
 * )
 */
class LanguageCondition extends ConditionBase implements ContainerFactoryPluginInterface {

  /**
   * The Language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Creates a new Language instance.
   *
   * @param array $configuration
   *   The plugin configuration, i.e. an array with configuration values keyed
   *   by configuration option name. The special key 'context' may be used to
   *   initialize the defined contexts by setting it to an array of context
   *   values keyed by context names.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->languageManager = $language_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('language_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return ['langcodes' => []] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    if ($this->languageManager->isMultilingual()) {
      // Fetch languages.
      $languages = $this->languageManager->getLanguages();
      $langcodes_options = [];
      foreach ($languages as $language) {
        $langcodes_options[$language->getId()] = $language->getName();
      }
      $form['langcodes'] = [
        '#type' => 'checkboxes',
        '#title' => $this->t('Language selection'),
        '#default_value' => $this->configuration['langcodes'],
        '#options' => $langcodes_options
      ];
    }
    else {
      $form['langcodes'] = [
        '#type' => 'value',
        '#default_value' => $this->configuration['langcodes'],
      ];
    }
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);
    $values = $form_state->getValue($form['#parents']);
    $this->configuration['langcodes'] = array_filter($values['langcodes']);
  }

  /**
   * {@inheritdoc}
   */
  public function evaluate(EntityInterface $entity) {
    $language = $this->languageManager->getCurrentLanguage();
    // Language visibility settings.
    return !empty($this->configuration['langcodes'][$language->getId()]);
  }

}

Not sure if it would make sense for this to live directly in commerce?

johnpicozzi’s picture

Ran into this issue today. Does anyone have any plans to develop a working patch to allow for Shipping methods (Rate Label) to be translated?

bojanz’s picture

Like all issues worth fixing, this one has gone in many different directions :)

We have two main problems here:
1) The shipping method entity can't be translated (e.g. the name field at least)
2) The shipping method plugin configuration can't be translated, and the rate_label is there.

We need to fix #1 first, by porting the fix I just committed for promotions (#2986802: Promotions can't be translated).

Unfortunately there is no good way to translate plugin configuration, because we have no mechanism for partial translations (translate the rate label, but take the rate amount from the original). So I suggest a workaround: introduce a translatable display_name field (we can copy #2770731: Add a display name to promotions once it lands), then use it to replace the rate_label (and move the data there). We can also add a needs_display_name flag to the annotation, and use it to show/hide the display_name based on the selected plugin.

bojanz’s picture

Here's the first part of the fix, which makes the shipping method translatable.
Make sure to apply it to -dev, because I had to do #3076739: Shipping methods shouldn't be under /admin/commerce/config when they're not config first to shorten the urls.

I'll roll the second part once #2770731: Add a display name to promotions lands to Commerce.

Berdir’s picture

+++ b/src/Entity/ShippingMethod.php
@@ -54,12 +59,26 @@ use Drupal\Core\Field\BaseFieldDefinition;
+   */
+  public function toUrl($rel = 'canonical', array $options = []) {
+    if ($rel == 'canonical') {
+      $rel = 'edit-form';
+    }
+    return parent::toUrl($rel, $options);
+  }

I think media entity in core solves this by setting canonical also to the edit URL, afaik that requires fewer customizations and workarounds to make this work.

bojanz’s picture

@Berdir
I was worried that might confuse the route provider. I'll give it a shot.

EDIT: Tried it and it did confuse the route provider, I get an empty View tab. MediaRouteProvider overrides getCanonicalRoute() to make it work. So it's not really shorter.

bojanz’s picture

Okay, my plan from #17 (adding a display_name and hiding it for non-flat-rate) ended up being clunky. Plus, we now also have a rate_description field to think of (added in #3055937: Allow shipping rates to have a description, shown below the radio button).

So, I've decided to make the plugin configuration translatable instead.
The main use case for translation is flat rate, and flat rate has precisely three settings:
- rate_label (translatable)
- rate_description (translatable)
- rate_amount (doesn't hurt to be translatable, can even help with multi-currency).
So, I don't really see the downside.

Attaching a complete patch, and a screenshot.

I played with the idea of preventing the plugin itself from being changed on the translate form, but that would require doing the same on the normal edit form, and I wasn't sure if people were fine with that. Happy to revisit that before the issue is committed.

Berdir’s picture

> The main use case for translation is flat rate, and flat rate has precisely three settings:

Hm, wouldn't you most likely give a shipping method that uses an API a label like "Express shipping" that needs to be translated as well? That would then likely have settings too that would be a bit annoying to keep in sync between translations and you have to keep that in mind.

Didn't try yet, not saying it's a reason not to do it or anything, just might not be quite as simple as that :)

bojanz’s picture

@Berdir
API shipping methods tend to support multiple shipping services, defined in the plugin, so the label tends to come from there. Some (like Canada Post) take the service label directly from the API response. So far I haven't seen any that define a display-label-like field.

eglaw’s picture

@bojanz
the latest patch #21 doesn't seem to apply on the 2.0.0-beta8 release am i right?
Should i use the dev branch?

bojanz’s picture

Here's a reroll now that #3107367: Allow shipping promotions to be display-inclusive has landed.

As mentioned in #18, there is no easy way to produce a patch against beta8. Keep in mind that -dev breaks remote shipping methods (FedEx, UPS, etc), they will require patches.

FiNeX’s picture

Hi, on latest -dev the patch throws an error on checkout page.

The website encountered an unexpected error. Please try again later.
TypeError: Argument 2 passed to Drupal\commerce_shipping\ShipmentManager::__construct() must implement interface Drupal\Core\Entity\EntityRepositoryInterface, instance of Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher given, called in /var/www/html/web/core/lib/Drupal/Component/DependencyInjection/Container.php on line 277 in Drupal\commerce_shipping\ShipmentManager->__construct() (line 55 of modules/contrib/commerce_shipping/src/ShipmentManager.php).

Drupal\commerce_shipping\ShipmentManager->__construct(Object, Object, Object) (Line: 277)
Drupal\Component\DependencyInjection\Container->createService(Array, 'commerce_shipping.shipment_manager') (Line: 173)
Drupal\Component\DependencyInjection\Container->get('commerce_shipping.shipment_manager') (Line: 94)
Drupal\commerce_shipping\Plugin\Field\FieldWidget\ShippingRateWidget::create(Object, Array, 'commerce_shipping_rate', Array) (Line: 122)
Drupal\Core\Field\WidgetPluginManager->createInstance('commerce_shipping_rate', Array) (Line: 110)
Drupal\Core\Field\WidgetPluginManager->getInstance(Array) (Line: 154)
Drupal\Core\Entity\Entity\EntityFormDisplay->getRenderer('shipping_method') (Line: 175)
Drupal\Core\Entity\Entity\EntityFormDisplay->buildForm(Object, Array, Object) (Line: 277)
Drupal\commerce_shipping\Plugin\Commerce\CheckoutPane\ShippingInformation->buildPaneForm(Array, Object, Array) (Line: 559)
Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesBase->buildForm(Array, Object, 'order_information')
call_user_func_array(Array, Array) (Line: 520)
Drupal\Core\Form\FormBuilder->retrieveForm('commerce_checkout_flow_multistep_default', Object) (Line: 277)
Drupal\Core\Form\FormBuilder->buildForm(Object, Object) (Line: 218)
Drupal\Core\Form\FormBuilder->getForm(Object, 'order_information') (Line: 115)
Drupal\commerce_checkout\Controller\CheckoutController->formPage(Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 573)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 151)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 130)
Drupal\cdn\StackMiddleware\DuplicateContentPreventionMiddleware->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 694)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
bojanz’s picture

@FiNeX
You forgot to clear cache.

FiNeX’s picture

@bojanz: you're right, I forgot that :-(

  • bojanz committed 9e7e79b on 8.x-2.x
    Issue #2934142 by bojanz, StryKaizer, trebormc: Shipping method entity...
bojanz’s picture

Status: Needs review » Fixed

16 days have passed with no feedback, so I'm going to go ahead and commit the current patch.

Feel free to add comments post-commit, and/or open followup issues.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

tcrawford’s picture

Our solution in case it helps the community:

Our customer wishes to translate the shipping methods including rate plugin label and description. However, some of our rate plugins have a large number of 'amount' fields that should not be translated, as this would lead to duplicate amounts that need to be managed per language. Similarly, making language a condition means that a large number of 'almost redundant' shipping methods need to be created to manage translations. We solved this problem by adding some additional fields 'checkout_label' and 'checkout_description' and a boolean to the shipping method to allow the user to display these fields on the checkout rather than the plugin's rate label and description. We then subscribe to the ShippingEvents:SHIPPING_RATES event to override the values on the checkout.

/**
   * Override the rate label and rate description conditionally.
   *
   * The shipping method plugins should be set to non translatable.
   * This avoids needing to translate all rate values and manage these against
   * each translation.
   *
   * @param \Drupal\commerce_shipping\Event\ShippingRatesEvent $event
   *   The shipping rates event.
   */
  public function overrideRateLabelAndDescription(ShippingRatesEvent $event): void {
    $shippingMethod = $event->getShippingMethod();
    $shouldOverride = $shippingMethod->get('override_plugin_fields')->value;

    if (!$shouldOverride) {
      return;
    }

    $rates = $event->getRates();
    /** @var \Drupal\commerce_shipping\ShippingRate $rate */
    $rate = reset($rates);
    $newRates[] = new ShippingRate(
      [
        'shipping_method_id' => $shippingMethod->id(),
        'service' => new ShippingService(
          $rate->getService()->getId(),
          $shippingMethod->get('checkout_label')->value
        ),
        'amount' => $rate->getAmount(),
        'description' => $shippingMethod->get('checkout_description')->value,
      ]
    );
    $event->setRates($newRates);
  }
joro78’s picture

In fact shipping methods supports translation out of the box.
You have to enable the translation for it under admin/config/regional/content-language
Look at the section of this site after checking the box and enable Plugin and Name translation.
Then the shipping method shall have an option to be translated.

joro78’s picture

FileSize
7 KB
50.89 KB
163.98 KB
joro78’s picture

joro78’s picture

FileSize
30.49 KB
trickfun’s picture

Same problem for me.
Only Name is translatable.
Both "rate label" and "rate description" aren't.

Shipping version 2.3

ConradFlashback’s picture

Category: Bug report » Feature request

Thanks for the work.
Up #37.
Rate label and description are fields shown in the front end.
Is it possible to add these fields in entity translation?

Luca Cattaneo’s picture

It wasn't clear to me on first impression.
If you enable the translation of the Plugin field (inside Shipping Method), you will be able to translate also Rate label and Rate description.
It works without problems.
Have a nice day

Anybody’s picture

DANGER! There's a major bug when using the "Hide non translatable fields on translation forms" option for the shipping methods!
When saving a translation, all the shipping method settings are wiped!

See #3419982: Translating a shipping method with untranslatable fields hidden, deletes the configuration