Question

In a custom module, I have the following code:

\Drupal::messenger()->addMessage(t("Email changed to %email.", ['%email' => $new_email]));

I issue more than a dozen different messages in this way.

I want a way to store these messages in the database with a way to list them and edit them, assign an ID or keyword to each message, and then do the equivalent of

\Drupal::messenger()->addMessage(message_id, parameters);

I'm sure I can accomplish this by creating a "message" content type with keyword and message fields, adding a message node for each message, and then writing a function in my custom module that loads a node by keyword, inserts any variables, and then calls \Drupal::messenger()->addMessage() to display it. I could also create a View to list all messages for editing purposes. But it seems like there should be a better way that doesn't involve adding messages to the content of the site.

Is there a module or a built-in Drupal facility that does this?

My reading hasn't uncovered anything that looks like it will do these things. I realize Interface translation will support some of what I'm looking for, but don't know that it can be used with \Drupal::messenger()->addMessage().

Was it helpful?

Solution 3

The suggestions by @Taggart Jensen, @NoSssweat, and @Jaypan to use custom entity types are clearly good ones, given my original question. As I began working on a solution using custom entities, it dawned on me that I could use the t() function and User interface translation (which I already use for string overrides) to do what I want.

My goal:

Make it easy for editors to change the wording of messages displayed by our custom rsc module. Stop unwittingly adding a new entry to the interface translation table each time I change a message in my module code.

My solution:

I created a simple helper function in my rsc module to display messages by passing a key to addMessage() instead of an actual message:

function rsc_message($key, $args=[])
{
  \Drupal::messenger()->addMessage(t('rsc-' . $key, $args));                    
}

I realize this isn't especially brilliant, but I wanted a way to consistently prepend rsc- to each key. An example of a call to this function is:

rsc_message('option-expired',
[
  '@option' => $option,
  '@date' => date("F j", $expireTime),
  '@time' => date("g:i a", $expireTime)
]);

The corresponding entry in User interface translation (once the key gets added to the translation table and once I enter the "translation" shown below) will be:

rsc-option-expired | The option <strong>@option</strong> expired on @date at @time.

Passing a key to rsc_message rather than a displayable message turns User interface translation into a string manager. It's easy to find my keys using String contains in the manager because they all begin with rsc-.

Goodnesses of using this approach:

  • I don't have much extra code to maintain.
  • Editors modify our custom rsc module strings using the same manager they use for other strings.
  • All of my message strings (25 or so of them) can be viewed at the same time.
  • I don't create a new entry in the translation table every time I change a string in my rsc module code. The key stays the same; we just change the translation via the manager.
  • I can directly include HTML tags in the translation, so I don't have to fuss with the @var, %var, !var t() function stuff.
  • I can export the translations for bulk editing or reuse.

Badnesses of this approach:

  • A key isn't added to the translation table until the message for that key gets displayed. Sometimes getting a particular error message to be displayed can be a challenge.
  • I can't enter a translation into the table for a key until the key is there.

To overcome the badnesses, I created a quick-and-dirty string-manager feature in my custom rsc module as page string-manager. It allows me to add strings to the table, remove strings from the table, and test strings in the table to make sure they work right. Here's the code.

In modules/custom/rsc/rsc.routing.yml

rsc.stringManager:

  path: '/string-manager/{key}/{p1}/{p2}/{p3}/{p4}/{p5}'
  defaults:
    _controller: 'Drupal\rsc\Controller\rscController::stringManager'
    _title: 'String Manager'
    p1: ''
    p2: ''
    p3: ''
    p4: ''
    p5: ''
  requirements:
    _role: 'administrator'

In modules/custom/rsc/src/Controller/rscController.php

namespace Drupal\rsc\Controller;

use Drupal\Core\Controller\ControllerBase;

class rscController extends ControllerBase
{
  public function stringManager($key, $p1, $p2, $p3, $p4, $p5)
  {
    require_once DRUPAL_ROOT . '/modules/custom/rsc/rsc_string_manager.php';

    return
    [
      '#markup' => rsc_string_manager($key, $p1, $p2, $p3, $p4, $p5)
    ];
  }
}

In modules/custom/rsc/rsc_string_manager.php

function rsc_string_manager($key, $p1, $p2, $p3, $p4, $p5)
{
  // Avoid accidental add or delete of non-rsc strings.
  // Prepend ! to a key to override this safety measure.

  if (strpos($key, '!') === 0) {
    $key = substr($key, 1);
  }
  else if (strpos($key, 'rsc-') !== 0) {
    return "Prefix $key with rsc- or use ! to override";
  }

  // Note: '++' in the query string gets converted to '  '

  if (strpos($key, '  ') == strlen($key)-2) {
    $key = rtrim($key, ' ');
    $action = 'add';
  }
  else if (strpos($key, '--') == strlen($key)-2) {
    $key = rtrim($key, '-');
    $action = 'delete';
  }
  else {
    $action = 'check';
  }

  $storage = \Drupal::service('locale.storage');
  $string = $storage->findString(['source' => $key]);

  if ($action == 'add') {
    if (!is_null($string)) {
      return "$key -- ALREADY EXISTS";
    }
    $string = new \Drupal\locale\SourceString();
    $string->setString($key);
    $string->setStorage($storage);
    $string->save();
    return "$key -- ADDED";
  }
  else {
    if (is_null($string)) {
      return "$key -- NOT FOUND";
    }  
    if ($action == 'delete') {
      $string->delete();
      return "$key -- DELETED"; 
    }
    else { // ($action == 'check')
      $mask = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
      $args = [$p1, $p2, $p3, $p4, $p5];
      $trans = t($key);
      $params = [];

       // Find placeholders in the "translated" string and match arguments
       // to them in their order in the url. We use only @ placeholders
       // because we can use bold, italics, etc. in the translated string.

      $i = 0;
      $p = 0;
      while ($i < count($args) && $args[$i] && ($p = strpos($trans, '@', $p)) !== false) {
        $q = strspn($trans, $mask, $p+1);
        $params[substr($trans, $p, $q+1)] = $args[$i];
        $p += $q;
        $i++;
      }
      return t($key, $params);
    }
  }
}

Usage:

  • key++ (add a key)
  • key-- (remove a key)
  • key/p1/p2/... (check a translation)

Example:

  • mysite.com/string-manager/rsc-option-expired++ (add)
  • mysite.com/string-manager/rsc-option-expired (check)
  • mysite.com/string-manager/rsc-option-expired/retry/April 1/2:30 pm (check w/parameters)
  • mysite.com/string-manager/rsc-option-expired-- (delete)

Add the key (line 1 above). The displayed text is:

rsc-option-expired -- ADDED

Go to User interface translation, search for strings containing "rsc-", and find the newly added string. The string table contains:

rsc-option-expired | <no translation yet>

In the Translation for English column, paste in:

The option <strong>@option</strong> expired on @date at @time.

Now, the string table contains:

rsc-option-expired | The option <strong>@option</strong> expired on @date at @time.

Check the translation with no parameters (line 2 above). The displayed text is:

The option @option expired on @date at @time.

Check the translation with parameters (line 3 above). The displayed text is:

The option retry expired on April 1 at 2:30 pm.

Delete the key (line 4 above). The displayed text is:

rsc-option-expired -- DELETED

Now the string is gone from the table.

This utility has been quite useful to me as I've added messages to my custom rsc module. When I add a new message key, I use the utility to immediately add it to the translation table. Then I immediately add the translation (which editors will, no doubt, change later--who likes the way developers say things?)

Maybe this utility won't be useful to anyone but me; however, it took me a good while to figure out how to manipulate the translation string table properly, so maybe bits of the code will help someone.

P.S. By using ! before a key or by removing the rsc- requirement in the code, you can use this utility to manipulate any string in the translation table (as long as it doesn't contain slashes).

OTHER TIPS

The first thing to determine is whether you need your messages to be content or configuration. Content is (usually not) migrated between environments. It is created on an environment, and is unique to that environment. So your development environment may have one set of messages used for testing, and your production environment has another set of messages, used live.

Configuration on the other hand is migrated between environments. So you would create the messages on one environment, and when configuration is exported from that environment, the existing messages would be included in that export.

You probably want content entities.

How to create a new content entity type: https://www.drupal.org/docs/8/api/entity-api/creating-a-content-entity-type-in-drupal-8

How to create a new configuration entity type: https://www.drupal.org/docs/8/api/configuration-api/creating-a-configuration-entity-type-in-drupal-8

If you create a new content entity type, give the entity two fields, message_key and message_value. You will then load the entity as follows:

$entity = \Drupal::entityTypeManager()->getStorage('my_entity_type_id')->loadByProperties([
  'message_key' => $message_key,
]);

$message_value = NULL;
if ($entity) {
  $message_value = $entity->get('message_value')->value;
}

If you create a configuration entity, the ID of the entity will be the message key, and the entity only needs one more field for message value. The code above would be adjusted slightly:

$entity = \Drupal::entityTypeManager()->getStorage('my_entity_type_id')->loadByProperties([
  'id' => $message_key,
]);

$message_value = NULL;
if ($entity) {
  $message_value = $entity->get('message_value')->value;
}

The entity type you choose will become the storage mechanism for your messages, which you can then recover with the above methodology. Don't run $message_value through the t() function, as variables should not be run through that function, and translations of the message values are handled as translations of the entity, rather than the t() function.

You could do it with Entity Construction Kit (ECK)

This allows you to create a custom entity and add fields to it just like a Node content type, but without having revisions, a title, an author, and some other bloat that comes by default with nodes if you don't want them.

Here is a D8 tutorial of it.

Unfortunately, the D8 version does create a view route, while in the D7 version this was optional, but you can take away permission to view it.

Licensed under: CC-BY-SA with attribution
Not affiliated with drupal.stackexchange
scroll top