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).