Jun 10, 2014

Better use of the t() function for translators

When coding the use of the t() function to indicate strings that may need to be translated, it's much better to prepare and pass a complete message rather than fragments of a message.

As pointed out by the source articles below, one compelling reason for doing so is that a single string that contains a complete message provides context that might otherwise be missing. Context is extremely important in understanding the words and phrases that are used so that both an accurate and a syntactically correct translation might be provided.

For example,
drupal_set_message(t('Enter your ' .
    l(t('account number'), '/mymodule/settings') .
    t('. There are also the ') .
    l(t('permissions'), '/permissions/module-mymodule') .
    t(' to set for specific roles.')),
  'status');
As I understand it, the above code would result in five different strings to be translated that the translator might not readily know are really part of a single message.

1)  '. There are also the '
2)  ' to set for specific roles.'
3)  'Enter your '
4)  'permissions'
5)  'account number'

A second reason that the above code is problematic is that it assumes that the word order of the target languages is the same as in English. Not so! For instance, in Korean, the main verb of a sentence is normally at the very end. In Swahili, adjectives and other noun modifiers almost always come after the noun, not before. The five strings that are concatenated above are in a fixed order that cannot be changed except by modifying the code.

Here's a different way to code this to enable better translations.
drupal_set_message(t('Enter your ' .
    '<a href="@url_1">account number</a>.' .
    ' There are also the ' .
    '<a href="@url_2">permissions</a>' .
    ' to set for specific roles.',
    array('@url_1' => url('/mymodule/settings'),
          '@url_2' 

              => url('/permissions/module-mymodule'))),
  'status');
There is a single string and a single call to the t() function. The translator can change word order in any way appropriate, including the two HTML tags, whose targets are generated.

The downside is that the translator must be able to handle a bit of HTML since the string they will see is:
Enter your <a href="@url_1">account number</a>. There are also the <a href="@url_2">permissions</a> to set for specific roles.
See the sources below for more examples and discussion.

Update for Drupal 8, beta 1: function url() has been replaced. See http://optimizely-to-drupal-8.blogspot.com/2014/11/beta-1-released-more-changes.html

Sources:

function  t
https://api.drupal.org/api/drupal/includes!bootstrap.inc/function/t/7

function  l
https://api.drupal.org/api/drupal/includes!common.inc/function/l/7

Dynamic strings with placeholders
https://drupal.org/node/322732

Jun 9, 2014

Issue: Uninstalling a module in the presence of a shortcut for it

When I started working on the .install script, I first tried to uninstall the module. This completely broke the site and made it unusable. Only a page with an error message would be displayed no matter what path I tried. The message stated that a RouteNotFoundException was thrown for route optimizely.add_update.

The full error message is:
Symfony\Component\Routing\Exception\RouteNotFoundException:
Route "optimizely.add_update" does not exist.
in Drupal\Core\Routing\RouteProvider->getRouteByName()
(line 150 of core\lib\Drupal\Core\Routing\RouteProvider.php). 
Searching through the database, I found a row in table shortcut that referred to that route. After deleting the row, the site worked fine.

I don't remember creating such a shortcut - I didn't even know about shortcuts before this problem happened - and don't know how it came into being.

But I was able to reproduce this by manually adding a shortcut for the path to one of the Optimizely tabs, then uninstalling the module. The same error message came up.

I haven't found any recourse other than to go into the database to delete the shortcut from the shortcut table.

The first article below reports this issue along with a recent attempt to fix this in core.

Sources:

RouteNotFoundException when a module (previously added to shortcuts) disabled
https://drupal.org/node/2266325

Working with the shortcut bar
https://drupal.org/documentation/modules/shortcut




Jun 8, 2014

The .install script: hook_schema(), hook_install(), hook_enable(), etc.

Converting the optimizely.install script:

hook_schema() works as is under Drupal 8 without any changes.

hook_install() works as is except that the  st() function which is used within installation scripts has been removed in Drupal 8. The  t() function is used instead.

In D7 you could disable a module without uninstalling it, but with Drupal 8 you can only uninstall. The source articles below describe numerous problems that can happen when a module is disabled but not uninstalled, so this is changed for D8. A module is either installed or not installed.

Consequently, hook_enable() and hook_disable() have been removed. Functionality that was previously in hook_enable() may need to be moved to hook_install(). Functionality that was previously in hook_disable() may need to be moved to hook_uninstall().

Upon uninstalling, hook_uninstall() automatically removes tables defined in hook_schema() as in D7.

I removed the existing several implementations of  hook_update_N(), for the moment just creating optimizely_hook_8000(). The function only sets a status message since the schema is not changing during this conversion.


Sources:

Modules cannot be in a disabled state anymore, only installed and uninstalled
https://drupal.org/node/2193013

Disabled modules are broken beyond repair so the "disable" functionality needs to be removed
https://drupal.org/node/1199946

Removed st() and get_t(), just use t() in place, simple!
https://drupal.org/node/2021435


Jun 4, 2014

Theme functions are gone, more or less

For the Project Listing tab of the module, the D7 version defines an item in  hook_menu()  that names function  optimizely_project_list_form()  to be called to create the form.

(1)  That function first queries the database to collect the raw data for existing projects.

(2)  Through the #theme property, the function specifies use of a theme hook that is declared in hook_theme(). The theme hook is implemented by a theme function, which builds most of the render array.

(3)  The theme function makes an explicit call to the core theme() function to do the actual rendering, passing table as the first param for that call.

Because there are indications that in Drupal 8 the use of theme functions is strongly discouraged, calling  theme() directly is discouraged, and because the D7 implementation seemed unnecessarily complex, I studied the code to see if I could come up with an alternative.

The upshot is that I was able to refactor the code to avoid what is deprecated and to make the call structure a little simpler.

Keep in mind that for implementing forms, we are now dealing with a class derived from FormBase.

(1)  The buildForm() method first accesses the database to collect raw data, exactly as before.

(2)  For the #theme property of the render array, the method provides a value of table. No theme hook is used, nor is there a call to theme().

(3)  The gut logic that was previously in the theme function was extracted into a private method of the same class that implements the form. This new method is directly called once for each project, i.e. each row of the table, to provide its addition to the render array.

One notable thing about this conversion task is that I was able to copy-paste large chunks of code as is. It was only the overall flow that was changed. The contents and the details of the resulting render array were almost exactly the same, as were almost all of the control structures.

Sources:

Completely new theme/template system: Twig
https://drupal.org/node/1831138

Jun 2, 2014

Twig template with "if" conditional

The next step I took was to implement the use of a D7 template file that contains conditional markup wrapped by PHP, converting it to Twig. The original code looked something like the following. I have removed most of the markup, keeping just enough to illustrate the point.
<p>The basic configuration and design ... </p>

<?php if (($variables['form']['optimizely_project_code']['#default_value'] == 0) && ($variables['form']['optimizely_oid']['#value'] == 1)): ?>

  <p>In order to use this module, ... </p> 
  <p>The default Project ...  </p>


<?php endif; ?>

<?php echo drupal_render_children($form)

Here's a version for Twig.
<p>The basic configuration and design ... </p>

{% if form['optimizely_project_code']['#default_value'] == 0 and form['optimizely_oid']['#value'] == 1 %}

  <p>In order to use this module, ... </p> 
  <p>The default Project ...  </p>


{% endif %}

{{ form }}

As in the previous post, {{  }} is used to print the expression that is enclosed, which may be a variable name, a string literal, etc.

There is an alternate syntax for array expressions that is more compact and a little easier to read. The following two lines are equivalent.
form['optimizely_oid']['#value']
form.optimizely_oid['#value'] 
But the next line results in a syntax error because of the pound sign # so you can't always use this dot notation.
form.optimizely_oid.#value

Note:  <?php  ?>  delimiters and their enclosed PHP code are ignored in Twig templates.

Sources:

TWIG > tags > if
http://twig.sensiolabs.org/doc/tags/if.html

Creating and Using Templates
http://symfony.com/doc/current/book/templating.html