Monday, October 28, 2019

Drupal 7 / Ubercart 7 - Multilingual User Activation Emails

Drupal core 7.67 / Ubercart 7.x-3.13 


Following years of frustratingly hard-to-reach, disinterested and/or incompetent and needlessly expensive Drupal developers we have set up an affordable commercial service for Small and Medium sized Enterprise (SME) decision-makers who rely on Drupal to support their business, like us.  So, if you need help with this problem, or any other form of Drupal Wizardry, feel free to contact us via our new division, Drupal Wizard, which provides support for all versions of Drupal.  

We offer the following services to serve the unique and evolving needs of your business:
  • 24/7 Emergency Support
  • Low-Cost Annual Support Packages
  • Drupal Module Integration and Development
  • Linux and Drupal Scheduled Systems Maintenance
Visit www.drupalwizard.com for more information.

Multilingual User Activation Emails

Our Drupal 7 / Ubercart 7 eCommerce website attracts customers who operate in three different written languages.  

By order of popularity, they are:


  • Traditional Chinese
  • Simplified Chinese
  • English

So when a new customer comes to us, we would like them to tell us what their preferred language is just once, and from that moment on be operating in that language forevermore.

More generally, we want our new customer experience to go like this:



  1. Customer comes to website
  2. Customer chooses preferred language
  3. Customer browses website 
  4. Customer drops desired items in Shopping Cart
  5. Customer chooses to Checkout
  6. Customer Registers
  7. Customer Validates
  8. Customer Pays
  9. Customer is updated regarding order status
  10. Customer receives goods

To facilitate this, and make things as easy as possible for our new customers, we have placed a Language switcher at the top of our Home page:


Let's see how this actually goes:





Drupal 7 / Ubercart 7 - Remove Special Discounts from Ubercart Checkout

Drupal core 7.67 / Ubercart 7.x-3.13 


Following years of frustratingly hard-to-reach, disinterested and/or incompetent and needlessly expensive Drupal developers we have set up an affordable commercial service for Small and Medium sized Enterprise (SME) decision-makers who rely on Drupal to support their business, like us.  So, if you need help with this problem, or any other form of Drupal Wizardry, feel free to contact us via our new division, Drupal Wizard, which provides support for all versions of Drupal.  


We offer the following services to serve the unique and evolving needs of your business:
  • 24/7 Emergency Support
  • Low-Cost Annual Support Packages
  • Drupal Module Integration and Development
  • Linux and Drupal Scheduled Systems Maintenance
Visit www.drupalwizard.com for more information.

Remove Special Discounts from Ubercart Checkout

We don't offer Special Discounts at our Drupal 7 / Ubercart 7 eCommerce store.

So we needed to remove this feature because it caused confusion in our customer base:



Unfortunately, this feature was enabled by default when we installed the Discount Coupons Module (uc_coupons), and it took a little detective work to figure out where this setting was and how to turn it off.

Here's how we disabled Special Discounts.

First, go to the right place in the Store Checkout Configuration area:



Then uncheck the Special discounts option:



Click on Save configuration

Now, the option no longer appears:






Sunday, October 27, 2019

Drupal 7 / Ubercart 7 - Remove Language Switcher Dropdown Undefined Index Errors

Drupal core 7.67 / Ubercart 7.x-3.13  / Stability Theme 

Following years of frustratingly hard-to-reach, disinterested and/or incompetent and needlessly expensive Drupal developers we have set up an affordable commercial service for Small and Medium sized Enterprise (SME) decision-makers who rely on Drupal to support their business, like us.  So, if you need help with this problem, or any other form of Drupal Wizardry, feel free to contact us via our new division, Drupal Wizard, which provides support for all versions of Drupal.  

We offer the following services to serve the unique and evolving needs of your business:
  • 24/7 Emergency Support
  • Low-Cost Annual Support Packages
  • Drupal Module Integration and Development
  • Linux and Drupal Scheduled Systems Maintenance
Visit www.drupalwizard.com for more information.


Remove Language Switcher Dropdown Undefined Index Errors


After installing the Language Switcher Dropdown Module, and using it for a little while, my Administrator interface began to be filled with these errors: 



Turns out this is just due to a bit of lazy coding and a lot of lazy fixing.  

A fix is readily available for this problem for two years now, that hasn't made its way into the module core code yet.  Shameful!  

The fix is really straightforward; it involves checking to see if the entity exists before using it.  Unfortunately, the "fix" remains a patch, which means that if you update the module and the fix hasn't been implemented, it will vanish.

In other words we are once again being forced to wildcat hack Drupal modules.

Here's an "OK" solution I found on the Drupal.org website, that avoids this error by simply checking that entity being used isn't empty(), which leads to a null pointer error (or some such).  There is a typo in the following fix, which I have corrected later on in this document.


diff --git a/lang_dropdown.module b/lang_dropdown.module
index 8e1a5c1..47e6114 100644
--- a/lang_dropdown.module
+++ b/lang_dropdown.module
@@ -452,7 +452,7 @@ function lang_dropdown_language_switch_links_alter(&$links, $type, $path) {
   foreach ($links as $langcode => $link) {
 
     $translation_access  = FALSE;
-    if (module_exists('entity_translation')) {
+    if (!empty($translations_data[$langcode]) && module_exists('entity_translation')) {
       $translation_access = entity_translation_access(
         $translations_data[$langcode]['entity_type'],
         $translations_data[$langcode]);


(from https://www.drupal.org/project/lang_dropdown/issues/2946147)

But you'll notice that the error in this case appears in two places, not one, so this particular fix might need to be applied in two places, potentially:


  • Notice: Undefined index: zh-hant in lang_dropdown_language_switch_links_alter() (line 457 of /var/www/vhosts/holisticpethelp.com/www/mobile/sites/all/modules/lang_dropdown/lang_dropdown.module).
  • Notice: Undefined index: zh-hant in lang_dropdown_language_switch_links_alter() (line 458 of /var/www/vhosts/holisticpethelp.com/www/mobile/sites/all/modules/lang_dropdown/lang_dropdown.module).

But, then again, maybe not.  

Here's the offending lines, and it turns out they are dependent on a single if statement.


    if (!empty($translations_data[$langcode]) &&
      (module_exists('entity_translation')) {
      $translation_access = entity_translation_access(
        $translations_data[$langcode]['entity_type'],
        $translations_data[$langcode]);

    }

So, fixing the if statement should resolve both errors.  Two for one!

Solving this problem was pretty simple once I had an idea of what the problem was, and where it was.

My Fallback Plan:

First, I documented the existing problem, leaving the original code in place as a fallback, which came in handy, because the original fix code crashed Drupal!

/*
//  GL  2019-10-27  Getting undefined index errors with this block of code - BEGIN
    if (module_exists('entity_translation')) {
      $translation_access = entity_translation_access(
        $translations_data[$langcode]['entity_type'],
        $translations_data[$langcode]);
    }
//  GL  2019-10-27  Getting undefined index errors with this block of code - END
*/

Next, I fixed the code by 

- Inserting !empty($translations_data[$langcode]) && into the if condition

//  GL  2019-10-27  Replacement block of code with !empty check added - BEGIN
    if ( !empty($translations_data[$langcode]) && module_exists('entity_translation') ) {
      $translation_access = entity_translation_access(
        $translations_data[$langcode]['entity_type'],
        $translations_data[$langcode]);
    }
//  GL  2019-10-27  Replacement block of code with !empty check added - END

And then tested the result.

Lo!  The error went away!  





Now we know how to get rid of these kinds of errors


REFERENCES:

https://www.drupal.org/project/lang_dropdown


https://www.drupal.org/project/lang_dropdown/issues/2946147

Saturday, October 26, 2019

Drupal 7 / Ubercart 7 - Choosing the Right Language Selector

Drupal core 7.67 / Ubercart 7.x-3.13 

Choosing the Right Language Selector

Our multi-lingual site needs to work correctly, by which I mean that the user must be presented an environment in their selected language at all  times.

When you enter our website, the interface presented is English, and one of the first things the user sees upon entering is a language selector:



As you can see, the three languages offered are:


  • Traditional Chinese (for customers primarily outside Mainland China)
  • Simplified Chinese (for customers primarily inside Mainland China)
  • English (for everyone in the world...hopefully)


Now, the way that translations work in Drupal is each node created is given a unique number, which is then related in the back end.

What the switcher is supposed to do is signal to Drupal that all  interface elements (menus, for example) are to be translated and to navigate to whatever node the switcher points to.

If the switcher gets it wrong, the site doesn't work - something we struggled with for a few days.


Eventually, the switcher that worked for us is this one:

The switchers that did not work for us were:



These switchers introduced copious errors in the Administration Interface, and/or bizzarre site behaviour that made the site translate incorrectly.



Friday, October 25, 2019

Drupal 7 / Ubercart 7 - Improperly Translated Country Dropdowns

Drupal core 7.67 / Ubercart 7.x-3.13 


Properly Translated Country Dropdowns

On our eCommerce  website, we have clients who come in expecting to have a user experience in their preferred language, of which there are three:


  • Traditional Chinese
  • Simplified Chinese
  • English

The problem we are facing here is the fact that the country dropdown in the checkout phase features a bunch of ENGLISH country names mixed into the CHINESE country names.
Here's what I mean.  In this example the user has composed a typical order in Chinese:



But clicking on the Proceed to Checkout button (written in Chinese, of course) renders this screen:



WTF?  Everything needs to be in Chinese!

Maybe what's happening is the countries are only being "half translated", with a bunch of English entries showing at the top because of the sort order of the dropdown population is in SQL, which counts ENGLISH words as lower id than CHINESE words, thus placing them at the top.


Here's a screenshot of the mixed up country list in Drupal 7 / Ubercart 7.



What's bizarre about this situation is that Hong Kong isn't translated, when it actually is a Chinese place.  Weird!




What might be happening is a bunch of untranslated junk entries are filling the dropdown.

So how do we fix this?


Looking under the hood, here's the table schema of the uc_countries table:

+--------------------+------------------+------+-----+---------+-------+
| Field              | Type             | Null | Key | Default | Extra |
+--------------------+------------------+------+-----+---------+-------+
| country_id         | int(10) unsigned | NO   | PRI | NULL    |       |
| country_name       | varchar(255)     | NO   | MUL |         |       |
| country_iso_code_2 | char(2)          | NO   |     |         |       |
| country_iso_code_3 | char(3)          | NO   |     |         |       |
| version            | smallint(6)      | NO   |     | 0       |       |
+--------------------+------------------+------+-----+---------+-------+


This looks pretty normal to me.

Looking at the entry for Hong Kong in the uc_countries table, I found this:

| 344 | Hong Kong | HK | HKG | 1 |

Again, everything looks fine to me.  Standard ISO codes are being used

So this really looks like some countries simply haven't been translated to Chinese (yet), and are therefore failling through the code to show up in the dropdown, untranslated.  

So, is this why Hong Kong is not being translated properly to 香港?

Let's have a look at the Translate Interface to find out.

Aha!  

The string Hong Kong was untranslated - so let's translate it:



Now, the translated term for Hong Kong shows up in the dropdown in Ubercart....yaay!



So the way to fix this problem is to get the translation strings for the all the untranslated countries loaded into Drupal 7 via the Translate Interface....hmmm

All 29 of them.  

Here they are, including the incredibly bizarre Nauru (諾魯) entry, which should by all rights be impossible, considering it's a mixed translation case.

Bolivia, Plurinational State of 
Bonaire, Saint Eustatius and Saba 
Brunei Darussalam 
Congo 
Congo, the Democratic Republic of the 
Côte d'Ivoire 
Falkland Islands (Malvinas) 
Heard Island and McDonald Islands 
Holy See (Vatican City State) 
Hong Kong
Iran, Islamic Republic of 
Korea, Democratic People's Republic of 
Korea, Republic of 
Lao People's Democratic Republic 
Macau 
Micronesia, Federated States of 
Moldovoa, Republic of 
Nauru (諾魯) 
Palestinian Territory, Occupied 
Russian Federation 
Réunion 
Saint Helena, Ascension and Tristan da Cunha 
Syrian Arab Republic 
Taiwan, Province of China 
Tanzania, United Republic of 
Venezuela, Bolivarian Republic of 
Viet Nam 
Virgin Islands, British 
Virgin Islands, U.S. 
Åland Islands

Some of these countries are of little size or consequence (Virgin Islands, U.S.) but others are seriously important and big places (Iran, Islamic Republic of ) or spots with equally a heavy-duty economy and top-tier global standing (Hong Kong).

So, we need a way to add 29 country translations because they are missing in the .po files.




Checking a Country Translation Entry

Seeing as I fixed the entry for Hong Kong already,  let's reproduce the error with another country we might be selling into one day:  Vietnam

First, let's check to see if this string is present in the String Translations database, the interface to which is located at:




Executing the query, we get the following response:



OK, let's enter the information for it in the interface



And then check the translation status:


What does a .po file look like?

The format of .po files are pretty simple.  Here's an example:

msgid "Hong Kong S.A.R., China"

msgstr "香港特别行政区,中国"

In the above example, the string Hong Kong S.A.R., China would be replaced by 香港特别行政区,中国 if the site was appropriately configured to be multilingual.

Our problem is that the strings that don't translate in the Ubercart checkout screen are not included in the .po file for Simplifed Chinese (zh-hans) or Traditional Chinese (zh-hant)

Let's make an entry for one of the other untranslated entries:  Viet Nam

Traditional Chinese

msgid "Viet Nam"
msgid "越南"

Simplified Chinese

msgid "Viet Nam"
msgid "越南"


REFERENCES

https://www.drupal.org/project/addressfield/issues/1405336

https://drupal.stackexchange.com/questions/214803/way-to-programmatically-add-string-translations

Drupal 7 / Ubercart 7 - How to Apply a Patch File

Drupal core 7.67 / Ubercart 7.x-3.13 


Implementing FUZZY Search in Drupal 7


Customers don't always remember the exact name of the product they are looking for, so they typically want to have FUZZY search capabilities, which means they enter a partial search term to get a list of candidate answers to then pick from.

A good example of this is a product we have that is very popular, but has a bit of a strange name:  OCU-GLO.  This name has a variant spelling and also a hypen in the middle, which leads to a lot of confusion if you've only heard the name, but not seen it.

Unfortunately, the default search behaviour of Drupal is exact search.  This is really stupid in an age where everyone is using Natural Language Processing (NLP) in their search, including Google - who has offered it for 20 years!

This is what happens on a stock Drupal 7 site when an incomplete term like "ocu" is used to initiate a search:



Stupid, stupid, stupid.

This feature has been requested by Drupal 7 users since 2009, yet it has not made itself into the stock Drupal 7 distribution, even as Drupal 7 is being "sunsetted" by its maintainers, who in my opinion are doing a terrible job supporting their user community.

Rant

Several of the features we need appear to still require CORE modifications, even seven years after the releast of Drupal 7.  Of course, this is very silly in our customer-centered era - but to be expected with such a disorganized setup as the Drupal organization, which features an achingly slow, authoritarian central planning committee and a fanatical but glaringly incomplete engineering and product focus to the point where its output is actually detrimental to the customer organization (i.e. putting Drupal in and getting it working wastes a lot of time).  So, ordinary users are forced to hack the source code of Drupal, even as Drupal high priests ominously intone that "hacking Drupal Core is forbidden behaviour" - while taking years to fix critical things!  Of course, this is incredibly frustrating for someone who has a business to run on Drupal, like us, mostly because we stupidly chose Drupal in 2010 and are now desperately trying to avoid the massive migration costs of moving across eCommerce platforms, even as they are forced to re-implement their entire back-end within the Drupal eCommerce platform because its upgrade model is basically broken.  Drupal really is the gift that keeps on taking.

How to Patch Drupal 

1.  Document the Patch

Extensively document the patch because you may need to re-implement it if you upgrade Drupal Core, and things will stop working after an upgrade so in the heat of your WTF you will want to go somewhere you wrote stuff with a cool head, like here on Blogger(tm), thus helping along others who are also struggling to make Drupal what it could be, rather than what it is.

2.  Determine Where to Apply the Patch

You can figure out where the patch is to be applied by looking at the header of the patch file.  Here's the location of the patch file we are going to use as an example:


https://www.drupal.org/files/issues/search-partial_words_hack-498752-41_0.patch

Here's the entire source code of the above file, just in case it disappears one day:


diff --git a/modules/search/search.extender.inc b/modules/search/search.extender.inc
index 4074256..827ca3a 100644
--- a/modules/search/search.extender.inc
+++ b/modules/search/search.extender.inc
@@ -285,7 +285,7 @@ class SearchQuery extends SelectQueryExtender {
         foreach ($key as $or) {
           list($num_new_scores) = $this->parseWord($or);
           $any |= $num_new_scores;
-          $queryor->condition('d.data', "% $or %", 'LIKE');
+          $queryor->condition('d.data', "%$or%", 'LIKE');
         }
         if (count($queryor)) {
           $this->conditions->condition($queryor);
@@ -297,7 +297,7 @@ class SearchQuery extends SelectQueryExtender {
       else {
         $simple_and = TRUE;
         list($num_new_scores, $num_valid_words) = $this->parseWord($key);
-        $this->conditions->condition('d.data', "% $key %", 'LIKE');
+        $this->conditions->condition('d.data', "%$key%", 'LIKE');
         if (!$num_valid_words) {
           $this->simple = FALSE;
         }
@@ -310,7 +310,7 @@ class SearchQuery extends SelectQueryExtender {
     }
     // Negative matches.
     foreach ($this->keys['negative'] as $key) {
-      $this->conditions->condition('d.data', "% $key %", 'NOT LIKE');
+      $this->conditions->condition('d.data', "%$key%", 'NOT LIKE');
       $this->simple = FALSE;
     }
 
@@ -366,7 +366,7 @@ class SearchQuery extends SelectQueryExtender {
     if (!empty($this->words)) {
       $or = db_or();
       foreach ($this->words as $word) {
-        $or->condition('i.word', $word);
+        $or->condition('i.word', '%' . $word . '%', 'LIKE');
       }
       $this->condition($or);
     }

Examine the header of the patch file, which shows the location of the file to patch:


diff --git a/modules/search/search.extender.inc b/modules/search/search.extender.inc
index 4074256..827ca3a 100644
--- a/modules/search/search.extender.inc
+++ b/modules/search/search.extender.inc

OK, the file in question is search.extender.inc, which lives in the /modules/search/ directory


3.  Verify Where to Put the Patch

Don't blindly believe the patch file paths.  Things change and, remember, there are TWO /modules directories in Drupal (yes, very stupid), so verifying where to put the patch file is necessary.

Figuring out where exactly to put the patch is pretty easy using the find command:

# pwd
/var/www/vhosts/holisticpethelp.com/www/mobile
# find . -name "search.extender.inc"
./modules/search/search.extender.inc

4.  Go to Where the Target File Is

Next, physically go to where the file to be patched is:

# cd modules/search/
# ls
search.admin.inc           search.extender.inc  search.pages.inc        search.test
search.api.php             search.info          search-results.tpl.php  tests
search-block-form.tpl.php  search.install       search-result.tpl.php
search.css                 search.module        search-rtl.css



5.  Back up the Target File



Create a copy of the file to be changed, because who knows if the patch will be successful, or even solve your problem - Remember, you are in "wildcatting" programmer mode, with no guarantee of help from the incredibly slow formal Drupal team



# cp search.extender.inc search.extender.inc.backup-2019-11-25
# ls
search.admin.inc                      search.module
search.api.php                        search.pages.inc
search-block-form.tpl.php             search-partial_words_hack-498752-41_0.patch
search.css                            search-results.tpl.php
search.extender.inc                   search-result.tpl.php
search.extender.inc.backup-2019-11-25  search-rtl.css
search.info                           search.test
search.install                        tests


6.  Install the Patch


You can put the patch in the same directory as the module to be patched using wget:



wget https://www.drupal.org/files/issues/search-partial_words_hack-498752-41_0.patch
--2019-10-25 10:05:31--  https://www.drupal.org/files/issues/search-partial_words_hack-498752-41_0.patch
Resolving www.drupal.org (www.drupal.org)... 151.101.10.217
Connecting to www.drupal.org (www.drupal.org)|151.101.10.217|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1650 (1.6K) [text/plain]
Saving to: ‘search-partial_words_hack-498752-41_0.patch’

100%[====================================================>] 1,650       --.-K/s   in 0s

2019-10-25 10:05:32 (182 MB/s) - ‘search-partial_words_hack-498752-41_0.patch’ saved [1650/1650]

7.  Verify Patch Installation

# ls
search.admin.inc                       search.module
search.api.php                         search.pages.inc
search-block-form.tpl.php              search-partial_words_hack-498752-41_0.patch
search.css                             search-results.tpl.php
search.extender.inc                    search-result.tpl.php
search.extender.inc.backup-2019-11-25  search-rtl.css
search.info                            search.test
search.install                         tests

8.  Apply the Patch (In Verbose Mode)

You can apply the patch using the patch command as described above, which will below, replacing hello.patch with the name of your particular patch file.  For those who are a bit paranoid and sceptical when it comes to Drupal (me), you might want to verify that the changes were actually made in code using the --verbose option, which tells you what patch is up to as it does its thing:

# patch --verbose < search-partial_words_hack-498752-41_0.patch
Hmm...  Looks like a unified diff to me...
The text leading up to this was:
--------------------------
|diff --git a/modules/search/search.extender.inc b/modules/search/search.extender.inc
|index 4074256..827ca3a 100644
|--- a/modules/search/search.extender.inc
|+++ b/modules/search/search.extender.inc
--------------------------
patching file search.extender.inc
Using Plan A...
Hunk #1 succeeded at 285.
Hunk #2 succeeded at 297.
Hunk #3 succeeded at 310.
Hunk #4 succeeded at 366.
done

9.  Validate the Patch Was Applied

Considering that 

  • There were four changes detailed in the above diff file (+/-)
  • Four Hunks were successful in the patch
I'd say that this patch application was a success.

10.  Validate Desired Website Behaviour



REFERENCES:

https://www.thegeekstuff.com/2014/12/patch-command-examples/


https://www.drupal.org/project/drupal/issues/498752