Hello Everyone,

The upcoming 4.14.0 release will have breaking changes for themes and plugins that display html and translation tokens via template variables. This post will outline the changes required to custom themes based on the changes made in the Harmony theme to serve as a guide for upgrading your own themes & plugins.

1. Escape all template variables via {variable}

The first and biggest change is the introduction of html and translation escaping of all variables in templates. Prior to 4.14.0 if you wrote {variable} in a template the value would be inserted into the html page unchanged. Benchpressjs supports escaping these variables but it was disabled to prevent a breaking change when the migration was done from templates.js to benchpressjs. We are now aligning with how most template engines work.

What this means for themes is if variable contained html then it will no longer be displayed properly. For example the content of posts is rendered via {posts.content} in partials/topic/post.tpl. Upgrading to 4.14.0 without fixing the template would result in:

blog-1.png

To fix this in 4.14.0, you will have to wrap the variable with double braces like {{posts.content}}. This will stop the escaping and the post content will display properly. Double braces should only be used on content that is html sanitized. In this case core renders the markdown to html and sanitizes it so it is safe. Do not use {{variable}} syntax on untrusted content.

The {{}} syntax should also be used on all helper functions that return html. One example for this is the buildAvatar function that renders the icon or picture of a user. So {buildAvatar(user, "24px", true)} should be changed to {{buildAvatar(user, "24px", true)}}. We've updated all these helpers to escape the values and attributes used. If you have custom helpers you should do the same in them. Below is an example of a helper that is used in header.tpl. It uses the same escape function that {myVar} uses.

function buildMetaTag(tag) {
  const name = tag.name ? `name="${escape(tag.name)}" ` : '';
  const property = tag.property ? `property="${escape(tag.property)}" ` : '';
  const content = tag.content ? `content="${escape(tag.content).replace(/\n/g, ' ')}" ` : '';

  return '<meta ' + name + property + content + '/>\n\t';
}

Refer to section 3, for a full list of changes that were made to Harmony theme to upgrade it to 4.14.0.

2. Migration to tx() and txEscape() helper functions

This is the second major change, and we're introducing it as part of an ongoing upgrade process to help avoid creating too much work for theme maintainers at once.

The way translation works in NodeBB is through a post render step. Our translator scans the entire html page and replaces translation tokens that look like [[namespace:key]] with their correct translation.

Here is roughly how it goes from request to final html presented to user.

  1. User requests /topic/1
  2. Load all required data from database
  3. Call filter:parse.post to turn post markdown into html
  4. Translator escapes the html from step 3, so tokens in the post content don't get translated
  5. Benchpress renders the html filling in all data
  6. Translator scans html replacing translation tokens with their translations
  7. Translator unescapes the result (reverses step 4)
  8. Final html sent to user

With the introduction of a helper function tx() instead of scanning the entire page, we can use the helper to translate tokens. For example instead of <button>[[topic:reply]]</button> we can use <button>{{tx("topic:reply")}}</button>. This approach is more efficient because only the translation tokens are processed.

With 4.14.0 we will still run the whole page translation to make migration easier, but for 4.15.0 we are going to remove that step, so the entire flow will be:

  1. User requests /topic/1
  2. Load all required data from database
  3. Call filter:parse.post to turn post markdown into html
  4. Benchpress renders the html, calls tx() to translate tokens
  5. Final html sent to user

We are introducing 2 new helper functions for templates:

  • tx() - translates the string or array passed in. This supports a variety of different usages:
Old New
[[namespace:key]] {{tx("namespace:key")}}
[[namespace:key, foo]] {{tx("namespace:key", "foo")}}
[[namespace:key, {arg1}, {arg2}]] {{tx("namespace:key", arg1, arg2)}}
{text} {{tx(text)}}

You can also pass an array to the tx() helper. For example if you have:

const myVar = ["namespace:key", "arg1", "arg2"];

This can be passed straight to tx in the template {{tx(myVar)}}

tx helper html escapes arguments and invalid translation keys.

  • txEscape() - escapes variable so it is not translated. This is a stop-gap solution until we remove the whole page translation. For example we use this on content that should not be translated. If someone puts [[namespace:key]] in their post it doesn't get translated. After 4.15.0 it will be safe to remove these since everything will use tx().

3. List of changes for Harmony Theme

Let's summarize the changes needed for 4.14.0.

  1. Wrap template helper functions that return html with {{}} instead of {}
  2. Replace {translationString} with {{tx(translationString)}} or {{tx(translationString)}}, if the translation contains html.
  3. Wrap API values that should not be translated with {{txEscape(content)}}
  4. (Ongoing) Replace old translation tokens [[namespace:key]] with {{tx("namespace:key")}}.

Refer to the Harmony Upgrade Changes Topic for the full list of changes.

4. Bootbox & app.renderAsync & Misc.

  1. The global bootbox object has been removed, there is a new modals module that also handles translations.
    Use modals.(dialog|confirm|alert|prompt) instead of bootbox variants. The only difference is these methods return a promise.
    So the old const modal = bootbox.dialog() becomes const modal = await modals.dialog()

  2. If you are using app.renderAsync to render a template it needs the translation context for tx() helper to work. You can pass the translation context to it by passing it along with the template parameters. For example:

const data = { foo: 1 };
const languages = nodebb.require('./src/languages);
const data._i18n = languages.getFull('en-GB');
const html = await app.renderAsync('mytemplate', data);
  1. config.assetBaseUrl was deprecated in 2.x, it is now removed, use config.asset_base_url

Let us know in the Technical Support category if you need help with updating your themes or plugins.