I appreciate this one is only a warning, and that there may be security reasons for insisting on the rule

This rule is at odds with object-oriented encapsulation:

WARNING | Only string literals should be passed to t() where possible

Example: in my educational OOE = Object Oriented Examples tutorial module (sandbox https://drupal.org/sandbox/webel/2120905) I have an interface IBlock and a class DefaultBlock that encapsulates a Drupal block. It has a private variable for the block $info, and this is used (with a lot of other stuff) to help build a Drupal block array that is then passes off to Drupal core:

class DefaultBlock implements IBlock {

  /**
   * From hook_block_info(): 
   * 'The human-readable administrative name of the block.'
   * 
   * 'This is used to identify the block on administration screens, 
   * and is not displayed to non-administrative users.'
   * 
   * @var string
   */
  private $info;

  public function __construct($delta, $info) {
..
    $this->info = $info;
  }

  /**
   * Builds and returns a Drupal array compatible with hook_block_info().
   * 
   * @return array 
   *   A Drupal array compatible with hook_block_info().
   */
  public function get() {
    $out = array();

    $out['delta'] = $this->delta;

    $out['info'] = t($this->info);
    // Coder: WARNING | Only string literals should be passed to t() ..

    if (!empty($this->cache)) {
      $out['cache'] = $this->cache;
    }
    if (!empty($this->properties)) {
      $out['properties'] = $this->properties;
    }
    if (!empty($this->weight)) {
      $out['weight'] = $this->weight;
    }
    if (!empty($this->status)) {
      $out['status'] = $this->status;
    }
    if (!empty($this->region)) {
      $out['region'] = $this->region->getName();
    }
    if (!empty($this->visibility)) {
      $out['visibility'] = $this->visibility;
    }

    $pages = $this->getPages();

    if (!empty($pages)) {
      $out['pages'] = $pages;
    }
    return $out;
  }

Coder triggers on this line:

$out['info'] = t($this->info);

Use of string literals is in such cases at odds with object-oriented encapsulation.

That said, I don't currently have any ideas how best to handle the above safely.

Comments

webel’s picture

Issue summary: View changes
yukare’s picture

This is because http://localize.drupal.org, as the tools used for translation need that the string is inside the function to parse it. If a string is a constant or a variable, it will be translated only if the same string is present anywhere on drupal.

webel’s picture

@yukare wrote:

.. If a string is a constant or a variable, it will be translated only if the same string is present anywhere on drupal. ..

Thanks for explanation.

klausi’s picture

Component: Review/Rules » Coder Sniffer
Status: Active » Closed (works as designed)

Passing variables directly to t() is wrong 98% of the time, that's why we have the warning. New contributors make this mistake a lot. Instead, you should use t() where you define the string literal. If it is purely user provided content, then it is recommended to look into the i18n module and its helper functions.

So I don't think we should remove this sniff, although it can throw false positives from time to time.

webel’s picture

Status: Closed (works as designed) » Active

Passing variables directly to t() is wrong 98% of the time

Unless you are using object-oriented programming with encapsulation, as per my example, and then it's right 98% of the time.

Instead, you should use t() where you define the string literal.

Which prevents a Class constructor or method of a Class applying translation on injected strings as policy; it puts the onus on the developer to always remember to do it before or as one passes arguments to a constructor or method of a class, instead of letting the class do it for you (every time, automatically, as a policy).

See for example: RenderT a renderer Class that always translates its markup.

And what about the case where the string to be translated is constructed dynamically ?

I have left his as minor, but please don't just close it until you can provide an argument that actually addresses my example convincingly.

yukare’s picture

When you pass the string, you pass inside t():

$class myclass = new myclass(t("String to translate"));
$class->doAnythingWithString();

Or when you create it:

$word = t("String to translate");
$class myclass = new myclass($word);
webel’s picture

@yukare

$class myclass = new myclass(t("String to translate"));

Good, yes, examples (or counter examples if you disagree with mine) !

We are on the same page concerning this one, but you can see that your examples require the developer to always apply the policy (remember to execute t() on a literal at the point of string creation), which is different from it being applied as automatic policy within a rendering Class.

The WARNING says "where possible":

WARNING | Only string literals should be passed to t() where possible

The fact is that if the string is encapsulated within a Class that applies translation as an internal policy (or according to an option flag as many of my renderer classes do) it's not "possible". I will just have to ignore this warning.

klausi’s picture

Status: Active » Closed (works as designed)

Another problem is that automatic translation extraction tools cannot populate localize.drupal.org with expressions such as t($variable). They can only find t('Some string literal value that should be translated'). And yes, developers must use t() or D8's translation service where they define their user facing strings.

So whenever you have a class that applies translation as a policy you are doing it wrong in 98% of cases, because the translatable strings cannot be discovered.

webel’s picture

@klausi Thanks for feedback, agree closed.

You wrote:

.. whenever you have a class that applies translation as a policy you are doing it wrong in 98% of cases, because the translatable strings cannot be discovered.

"wrong" for Drupal's current translation discovery strategy, not "wrong" for OO encapsulation and the DRY (Don't Repeat Yourself) principle.

Am exploring ways to leverage placeholders:

- Dynamic strings with placeholders

- Localization API

- t()

webel’s picture

Category: Feature request » Support request
Status: Closed (works as designed) » Needs review

I have only reopened this (now as a support request) to record the results of my investigation. Please close again on reading.

How to encapsulate and share Drupal-translatable strings and markup

I have explored various ways of handling valid Drupal-translatable strings in a way that can be shared across an application, so that the Don't Repeat Yourself (DRY) principle is respected.

For the sake of this illustration I have a class DemoCommon that should be usable by external client classes.

It will offer a marked up string that explains that one can hover over links with a mouse to obtain more info, which string should be translatable.

Firstly, some things that don't work and why:

class DemoCommon  {

  const MARKUP_TIP_HOVER_LINKS
   = 'For more information on each demo please hover the mouse over each link:';
  // No ! Can't be Drupal-translated using t(self::MARKUP_TIP_HOVER_LINKS).
..
}

This was the initial point of this issue post. The Drupal translation registry strategy
does not see the encapsulated shared string in t(self::MARKUP_TIP_HOVER_LINKS).

This also does not work, applying t() in the initialiser:

class DemoCommon  {

  const MARKUP_TIP_HOVER_LINKS
   = t('For more information on each demo please hover the mouse over each link:');
  //  No ! PHP const can't be result of calculation.
..
}

Because PHP const can't be result of calculation this fails, however note that PHP5.6 will apparently offer more initialisation options.

Just for the record, trying to make a static class variable act as a const in PHP does not work:

class DemoCommon  {

  final static private $MARKUP_TIP_HOVER_LINKS = ..
  // No ! Unlike Java, PHP does not support final on static class variables.

}

Unlike Java, PHP does not support final on static class variables.

One also can't initialise a static class variable with t() already:

class DemoCommon  {
  static private $MARKUP_TIP_HOVER_LINKS =
     t('For more information on each demo please hover the mouse over each link:');
  // No ! PHP5.3 does not support calculation in initializers.
}

I now show 3 strategies that work, with various pros and cons.

One can post-initialize a private static class variable like this (adapted from http://stackoverflow.com/questions/693691/how-to-initialize-static-varia...):

class DemoCommon  {

  /**
   * For demonstration of complex static init in PHP5.3.
   * 
   * It will be initialized with a Drupal-translated string.
   * 
   * Note that because it can't be set to final, it must
   * remain private, otherwise the outside world could
   * change it after initialization.
   * 
   * @var string
   */
  static private $T_MARKUP_TIP_HOVER_LINKS;

  /**
   * Demonstrates initialising complex static class variables.
   * 
   * Used here to initialize a shareable Drupal-tranlsated string.
   */
  static public final function init() {
    self::$T_MARKUP_TIP_HOVER_LINKS =
        t('For more information on each demo please hover the mouse over each link:');
  }

  static public function useMeInternallyOfferToPublic() { 
    $tip = self::$T_MARKUP_TIP_HOVER_LINKS;
    // Do something with $tip.
  }
}
DemoCommon::init();
// So complex static class variables initialized. 

This has the disadvantage that one can only safely use the private shared translated string internally (perhaps value adding before returning via another public method). This is because one can't in PHP make the static class variable final, and if it is made public it is not safe against external manipulation.

Another simpler approach is to use a public method that simply translates the string and returns it:

class DemoCommon  {
  /**
   * A translated shareable message about info when hovering mouse over links.
   * 
   * One way of sharing a Drupal-translatable string (DRY principle).
   * 
   * @return string
   *   A translated shareable message.
   */
  static public final function tMarkupTipHoverLinks() {
    return t('For more information on each demo please hover the mouse over each link:');
    // Could be combined with lazy init on a private static class variable.
  }
}

This can then easily be used externally as DemoCommon::tMarkupTipHoverLinks().

However, the following with lazy init gives better performance if it is used frequently:

class DemoCommon  {

  /**
   * For demonstration of complex static init in PHP5.3.
   * 
   * It will be lazily initialized with a Drupal-translated string.
   * 
   * @var string
   */
  static private $T_MARKUP_TIP_HOVER_LINKS_LAZY;

  /**
   * A translated shareable message about info when hovering mouse over links.
   * 
   * One way of sharing a Drupal-translatable string (DRY principle).
   * 
   * This version uses lazy initialization.
   * 
   * @return string
   *   A translated shareable message.
   */
  static public final function tMarkupTipHoverLinksLazy() {
    if (!isset(self::$T_MARKUP_TIP_HOVER_LINKS_LAZY)) {
      self::$T_MARKUP_TIP_HOVER_LINKS_LAZY =
          t('For more information on each demo please hover the mouse over each link:');
    }
    return self::$T_MARKUP_TIP_HOVER_LINKS_LAZY;
  }
}

This will be populated on first access as DemoCommon::tMarkupTipHoverLinksLazy().

As far as I can tell, all 3 solutions above are compatible with the Drupal localisation registry strategy (searching for t('string literal') in code) while also respecting the DRY principle (avoiding error prone repetition of the string and/or the translation of the string).

Any feedback welcome.

klausi’s picture

Status: Needs review » Fixed

I personally think that this is over-engineering just to conform to some interpretation of the DRY principle. Adding this kind of complexity just to not repeat a translatable sentence is not worth it in my opinion. One could argue this violates the KISS principle. But since we very seldom repeat translatable sentences anyway this will not be a problem for most developers.

All approaches you presented look fine to me if you really want to avoid repeating a translatable sentence. Thanks for the thorough research!

webel’s picture

Status: Fixed » Closed (works as designed)

Just feedback. I am changing this to Closed (works as designed) in the sense that the Drupal translation registry discovery process is by design not compatible with OO encapsulation of reusable text. I can dream up a number of other strategies that are, including sluggish real-time registration of text for translation, but I accept that I am going to have to live with it using one of the techniques I demonstrate at #10.