MODX Snippet Development Part 7

Using the MODX lexicon to provide translations that will allow our snippet to work in multiple languages.

By Bob Ray  |  Updated: April 3, 2024  |  5 min read
MODX Snippet Development Part 7

In the last article, we cleaned up our code, improved the design, and made the Snippet more flexible by adding properties to the Snippet tag. In this article we'll see how to "internationalize" our Snippet by providing translations of the language in message to be displayed.

Starting Point

For reference here's where we left off in the last article:

/* NewResource Snippet */

/* Default return value */
$output = '';

/* Optional placeholder */
$ph = $modx->getOption('ph', $scriptProperties, '', true);

/* Return value for new resources that are published */
$newOutput = $modx->getOption('showNew', $scriptProperties,
    '<span class="new_resource">NEW!</span>', true);

/* Set $interval as the number of seconds in two weeks */
$interval = $modx->getOption('interval', $scriptProperties, '2 weeks', true);

$cutoffTime = strtotime('now - ' . $interval);

if ($cutoffTime === false) {
    $output = '[NewResource] Invalid interval';
}

/* See if the resource is published */
$published = $modx->resource->get('published');

/* See if publication date is within the last two weeks */
if ($published && $cutoffTime) {
    /* Get the current time as a timestamp */
    $currentTime = time();

    /* Get the publishedon date and
       convert it to a unix timestamp */
    $ptime = strtotime($modx->resource->get('publishedon'));

    if ($ptime > $cutoffTime) {
        /* Yes, it's recent -- Set the return value */
            $output = $newOutput;
        }
    }
}

if (!empty($ph)) {
    $modx->setPlaceholder($ph, $output);
    $output = '';
}

return $output;

Internationalization

Now that our Snippet is in its final form, it's time to think about making it useful in other languages. This means using the MODX lexicon for all language strings that will be displayed (though in this Snippet, there aren't very many of them).

The MODX lexicon is actually very simple. A lexicon file has series of key/value pairs. When MODX sees a call to the lexicon containing a lexicon key, it checks the lexicon array in memory, looks up the value associated with that key and returns it. If it doesn't find one, it simply returns the key unchanged. We'll look at the format of a lexicon file in the bit, but first, let's look at how one is loaded.

Loading the Lexicon

In order for MODX to find our lexicon strings, they must first be loaded. Typically, there will only be one lexicon file for a small Snippet like ours and it will be called default.inc.php. The English version of the file would be loaded with this line near the top of the Snippet:

$modx->lexicon->load('en:newresource:default');

The argument is in three parts, separated by colons. The first part is the two-letter language code. The second is the namespace, and the third is the topic to load. MODX automatically appends .inc.php to the end of the topic when it looks for the lexicon file.

The namespace tells MODX where to look for the directory containing the language files. In order for it to work, you need to create a namespace in the Manager (System -> Namespaces) and set its core path. In our case, the name of the namespace would be newresource, and the path would be {core_path}components/newresource/. MODX will translate the {core_path} part when it gets the namespace path. That means everything will still work if you decide to move the MODX core directory or even if you move the whole site to a new server. Notice that there is never a slash between {core_path} and the directory name that follows it. In MODX, all standard paths end in a slash, so {core_path} will provide the necessary slash.

Lexicon File Location

So where is MODX going to look for our lexicon file? Because we've specified the language, the namespace (and it has a path), and the topic, it will look in this directory (where {core_path} is the actual path to the core):

{core_path}components/newresource/lexicon/en/default.inc.php

The directory structure for that file will look like this:

core
  components
    newresource
      lexicon
        en
          default.inc.php

In other words, it will always look for this file:

<i>{namespace_core_path}</i>lexicon/<i>{language}</i>/<i>{topic}</i>.inc.php

Dynamic Language Specification

Our multi-language version of the Snippet isn't going to do much good unless the user can specify the language to use in the Snippet tag. To make that happen, we need to get the two-letter language code from the &language property and use it in the $modx->lexicon->load() call. We'll use en as the default value:

$language = $modx->getOption('language', $scriptProperties, 'en', true);
$modx->lexicon->load($language . ':newresource:default');

If you have a multi-context site with a language for each context, you can create a cultureKey Context Setting for each context and do this in the Snippet tag:

&language=`[[++cultureKey]]`

Getting Fancy

Here is another, somewhat complicated, way of handling the language in the Snippet:

$language = $modx->getOption('language', $scriptProperties, '');
$language = !empty($language)
    ? $language
    : $modx->getOption('cultureKey', NULL,
        $modx->getOption('manager_language', NULL, 'en'));

$modx->lexicon->load($language . ':newresource:default');

In the code above, any language sent in the &language property of the Snippet tag will take precedence. If it is not set, the cultureKey System Setting will be used. If that isn't set, the manager_language System Setting will be used. Finally if none of those are set, the language will be set to 'en'.

Lexicon File Content

This couldn't be much simpler. Every line in every lexicon file takes this basic form:

$_lang['key'] = 'Value';

You use the key in your lexicon call. The value is what will be displayed to the user. It's a good practice to use a prefix with your lexicon keys so they won't collide with those of other extras or the entries in the MODX core lexicon. Well use the prefix nr_ for our Snippet.

Here is our English-language lexicon file for the Snippet

<?php
$_lang['nr_err_invalid_interval'] = 'Invalid interval';
$_lang['nr_show_new'] = 'NEW!!!';

As we mentioned above, the lexicon file will be located here:

core
  components
    newresource
      lexicon
        en
          default.inc.php

If we wanted to create a French language file, it might look like this (pardon my French):

$_lang['nr_err_invalid_interval'] = 'Intervalle non valide';
$_lang['nr_show_new'] = 'Nouveau!!!';

The French lexicon file would be located at:

core
  components
    newresource
      lexicon
        fr
          default.inc.php

The error message is returned by the Snippet with this code:

$output = $modx->lexicon('nr_err_invalid_interval');

I like to use lexicon keys that will be understandable if something goes wrong and the value is not found. This is especially important with error messages.

That takes care of the error message, but what about the 'NEW!!' indicator? Here is one way to handle that:

$output = $modx->lexicon('nr_show_new');

Notice that with this code, the &showNew property is no longer necessary in the Snippet tag. The user will set the value of it in the various languages with the entries in the lexicon files. This method is not optimum, however. If we put the full HTML for our output in the page content or template with the Snippet tag or the placeholder embedded in it, the outer part of the tag will be there regardless of whether the resource is new or not.

A better method is to continue to use the &showNew property with the language-specific part of it as a placeholder tag. The Snippet will then replace the placeholder tag itself before returning the output.

[[!NewResource?
    &showNew=`<span class="new_resource">[[+nr_show_new_ph]]</span>`
    &language=`en`
]]
$msg = $modx->lexicon('nr_show_new');
$output = str_replace('[[+nr_show_new_ph]]', $msg, $output);

Updated Code

This method requires almost no changes to our Snippet. We'll add the placeholder tag to our default output and add a couple of lines later to replace it in the output:

/* NewResource Snippet */

$language = $modx->getOption('language', $scriptProperties, 'en', true);
$modx->lexicon->load($language . ':newresource:default');

/* Default return value */
$output = '';

/* Return value for new resources that are published */
$newOutput = $modx->getOption('showNew', $scriptProperties,
    '<span class="new_resource">[[+nr_show_new_ph]]</span>', true);

/* Set $interval as the number of seconds in two weeks */
$interval = $modx->getOption('interval', $scriptProperties, '2 weeks', true);

$cutoffTime = strtotime('now - ' . $interval);

if ($cutoffTime === false) {
    $output = 'nr_err_invalid_interval';
}

/* See if the resource is published */
$published = $modx->resource->get('published');

/* See if publication date is within the last two weeks */
if ($published && $cutoffTime) {
    /* Get the current time as a timestamp */
    $currentTime = time();

    /* Get the publishedon date and
       convert it to a unix timestamp */
    $ptime = strtotime($modx->resource->get('publishedon'));

    if ($ptime > $cutoffTime) {
        /* Yes, it's recent -- Set the return value */
            $output = $newOutput;
            /* Replace the placeholder tag */
            $msg = $modx->lexicon('nr_show_new');
            $output = str_replace('[[+nr_show_new_ph]]', $msg, $output);
        }
    }
}

return $output;

Note that although we didn't use it in the example tag above, we could still send the &ph property in the tag and use the specified placeholder at the location where we want the entire output of the Snippet. It won't conflict with the nr_show_new_ph tag (even if it has the same name, though that would be a bad practice). The tag in the Snippet property will be replaced manually in the Snippet rather than being set as a placeholder.

Replacing placeholder tags manually in a Snippet is a common practice (Wayfinder does it, for example), because it's significantly faster than making MODX parse the placeholder tags. It also has the advantage of insulating you from the whims of the parsing order. This is important if you're nesting things (e.g. a Snippet tag inside a Chunk that's inside another Chunk). Generally speaking, anytime you can move actions into a Snippet and avoid having them handled by the MODX parser, you'll improve both performance and reliability.

It's also worth mentioning that the value of the &language property could be a tag, typically a TV tag so that you can set the language of a particular page by putting the two-letter language code in a TV.

Another option for setting the language is to have the Snippet get the language code from the URL of the page or from the current context. In the second case, you'd name the contexts with the two-letter language codes and put this line in the Snippet to set the $language variable:

&language = $modx->context->get('key');

If you prefer not to name the contexts with the language codes, you can also create a context Setting containing the context's language code (let's call the setting language_code), then do this in the Snippet:

&language = $modx->getOption('language_code');

Wrapping Up

This is the last article in this series. Over the course of these articles, we've seen how even a simple Snippet can develop over time, and we've explored the use of properties, time comparisons, and placeholders, among other things. We didn't discuss the use of Tpl Chunks and $modx->getChunk() to format Snippet output, but we've covered that in other articles.

In the next article, we'll look at another way to use the MODX lexicon to create language-specific content.

Bruno's Version

Here's a nice version of the Snippet created by Bruno Perner (Bruno17) and posted in the MODX forums. It lets the Snippet work properly with getResources.

Here's Bruno's code:

/* NewResource Snippet */

/* Default return value */
$output = '';

/* Return value for new resources that are published */
$newOutput = '<span class="label">NEW!</span>';

/* Set $interval as the number of seconds in two weeks */
$interval = 1209600;

/* See if the resource is published */
$published = $modx->getOption('published', $scriptProperties,
    $modx->resource->get('published'));

/* Get the publishedon date */
$publishedon = $modx->getOption('publishedon', $scriptProperties,
    $modx->resource->get('publishedon'));

/* For published documents, see if publication date is
   within the last two weeks */
if ($published) {
    /* Get the current time as a timestamp */
    $currentTime = time();
    /* Convert the publishedon date to a unix timestamp */
    $ptime = strtotime($publishedon);

    /* Get the resource's age in seconds */
    $age = $currentTime - $ptime;

    /* See if it's new */
    if ($age < $interval) {
        /* Yes, it's recent - return NEW! */
        $output = $newOutput;
    }
}
return $output;

Bruno's code doesn't have some of the enhancements we've looked at in this series, like lexicon strings, placeholders, and letting the user set the interval in the tag, but they should be easy to add.


Bob Ray is the author of the MODX: The Official Guide and dozens of MODX Extras including QuickEmail, NewsPublisher, SiteCheck, GoRevo, Personalize, EZfaq, MyComponent and many more. His website is Bob’s Guides. It not only includes a plethora of MODX tutorials but there are some really great bread recipes there, as well.