For Wt 4.14.0

1. Prerequisites

This introduction assumes that you have a reasonable understanding of Wt itself, in particular its stateful session model and the widget concept. If you haven’t done so yet, you may want to go through the Wt tutorial first.

The complete full code of the examples used in this tutorial are included in the Wt distribution, in examples/feature/localization.

2. Introduction

This tutorial explores the concept of localization in Wt. We’ll explain how to manage localized text with message resource files and WString, enabling your application to support multiple languages. We’ll also cover WTemplate in detail, showing you how to use its placeholder mechanism.

By the end of this tutorial, you’ll understand how localization works in Wt and how to use these tools effectively in your applications.

3. Message Resource and Localized WString

A message resource is an XML file containing messages with an id attribute. These messages are used for localization.

messages.xml
<!-- This is not necessary, but can be handy -->
<?xml version="1.0" encoding="UTF-8" ?>

<!-- The wrapper containing all messages -->
<messages>

  <!-- A single message entry, with an `id` to identify it, and content it displays -->
  <message id="language">English</message>

  <message id="title">Localization example</message>
  <message id="intro">
    This is an example showing how to use localized WStrings. This
    example is a complement to the tutorial on localization that can be
    found
    <a href="https://www.webtoolkit.eu/doc/tutorial/localization.html" target="_blank">
      here
    </a>.

    For each examples, the file used for the example is given. To
    really understand these examples, you will need to also read
    those files.
  </message>

</messages>

Message resources are loaded into the application’s WMessageResourceBundle using WMessageResourceBundle::use(). You refer to messages using WString::tr() with the message’s id. This creates a localized WString.

using messageResourceBundle (localization.C)
app->messageResourceBundle().use(appRoot() + "/messages");
app->messageResourceBundle().use(appRoot() + "/templates");

The used resources will not have their extension or (optional) suffix, when they are defined in the application.

A localized WString's value is resolved dynamically when needed by searching the WMessageResourceBundle for the corresponding id. This allows you to change the application’s language simply by providing different message resources.

When WMessageResourceBundle::use() is called, it automatically loads language-specific versions of the message resource file (e.g., messages_nl.xml, messages_fr.xml) if they exist in the same directory. This means using a bundle called strings, will prompt Wt to load the files: * string.xml * string_{locale}.xml

Here the locale can be any name of a valid locale (like nl or en). Wt automatically selects the appropriate resource file based on the name of the application’s WLocale. Changing the application’s WLocale triggers the use of the corresponding localized message resource file.

Here’s an example of a language selection dropdown button that uses this mechanism:

LanguageButton.C
LanguageButton::LanguageButton()
{
  setText(Wt::WString::tr("language"));
  auto popupMenu = std::make_unique<Wt::WPopupMenu>();
  popup_ = popupMenu.get();
  setMenu(std::move(popupMenu));

  popup_->addItem("English");
  popup_->addItem("Français");
  popup_->addItem("Nederlands");

  popup_->itemSelected().connect(this, &LanguageButton::onSelect);
}

void LanguageButton::onSelect(Wt::WMenuItem *item)
{
  Wt::WString lang = item->text();
  if (lang == "English") {
    Wt::WApplication::instance()->setLocale("en");
  } else if (lang == "Français") {
    Wt::WApplication::instance()->setLocale("fr");
  } else if (lang == "Nederlands") {
    Wt::WApplication::instance()->setLocale("nl");
  }
}

By default, the WApplication's WLocale is set based on the browser’s language preferences. Which means that the language the user configured is automatically selected.

For instance, the following WString is localized.

LocalStringExample.C
// Will change depending on the locale.
layout->addWidget(std::make_unique<Wt::WText>(Wt::WString::tr("LocalStringExample_baseText")));

It is defined in the default bundle:

LocalStringExample_baseText (messages.xml)
  <message id="LocalStringExample_baseText">
    This text changes depending on the language chosen
  </message>

Since the message is also found in both specific language bundles, it will be replaced with the correct value for the current WLocale.

LocalStringExample_baseText (messages_nl.xml)
  <message id="LocalStringExample_baseText">
    Deze tekst verandert afhankelijk van de gekozen taal
  </message>
LocalStringExample_baseText (messages_fr.xml)
  <message id="LocalStringExample_baseText">
    Ce texte change selon la langue choisie
  </message>

If a language-specific file is not found, the default messages.xml is used as fallback. This fallback also applies to the messages themselves. If a message is not found in a language-specific file, it is looked up in the default message resource file.

For instance, the following message is defined only in the default bundle and the French bundle but not in the Dutch bundle.

LocalStringExample_fallbackText (messages.xml)
  <message id="LocalStringExample_fallbackText">
    This text will fallback to English if Dutch is selected because it
    is not defined in messages_nl.xml
  </message>
LocalStringExample_fallbackText (messages_fr.xml)
  <message id="LocalStringExample_fallbackText">
    Ce texte sera en anglais si le néerlandais est sélectionné car il
    n'est pas défini dans messages_nl.xml
  </message>

Therefore, the following WString will fallback to the default message when Dutch is selected.

LocalStringExample.C
// Not defined in message_nl.xml. Will fallback to English if Dutch language is selected.
layout->addWidget(std::make_unique<Wt::WText>(Wt::WString::tr("LocalStringExample_fallbackText")));

In case the message is not defined in the default bundle, it will fallback to displaying ??key??, where key is the message’s id.

For instance, the following WString is not defined anywhere, and will therfore display ??undefined_message??.

LocalStringExample.C
// Is not defined anywhere. Will fallback to '??key??'.
layout->addWidget(std::make_unique<Wt::WText>(Wt::WString::tr("undefined_message")));

A message that is not defined in the default bundle can still be defined in a language-specific bundle. In that case, it will display ??key?? for all WLocale except the one for which it is defined.

For instance, the following message is defined in the Dutch and French bundles, but not in the default bundle.

LocalStringExample_noFallbackText (messages_nl.xml)
  <message id="LocalStringExample_noFallbackText">
    Deze tekst heeft geen terugvaloptie en wordt daarom als
    ongedefinieerd weergegeven als Nederlands of Frans niet is
    geselecteerd.
    (This text has no fallback, so it will show as undefined if Dutch
    or French is not selected.)
  </message>
LocalStringExample_noFallbackText (messages_fr.xml)
  <message id="LocalStringExample_noFallbackText">
    Ce texte n'a pas de version par defaut, il s'affichera donc comme
    indéfini si le néerlandais ou le français n'est pas sélectionné.
    (This text has no fallback, so it will show as undefined if Dutch
    or French is not selected.)
  </message>

Therefore, the following WString will display ??LocalStringExample_noFallbackText?? when English is selected. But will show the correct text when Dutch or French is selected.

LocalStringExample.C
// Is not defined in message.xml. Will fallback to '??key??' when English is selected.
layout->addWidget(std::make_unique<Wt::WText>(Wt::WString::tr("LocalStringExample_noFallbackText")));

3.1. Keeping WString localized

As seen above, a WString can be localized, meaning the value will be resolved when it is needed. This is in opposition with literal WString, which have a fixed value. For multilingual applications, you almost always want localized WString objects so they update when the WLocale changes.

However, you will sometimes not be able to use a localized WString directly. For instance, to create a WLink object, you need to use a std::string, which cannot be localized. Another situation like this is when you need to concatenate a localized WString. Regardless of what you concatenate to a localized WString, the result will always be literal. Even if you concatenate two localized WString together. For this reason, you will probably never want to use the += operator on a localized WString, as it will make it literal.

ConcatenationExamples.C
Wt::WString localWString = Wt::WString::tr("ConcatenationExamples_baseText");
Wt::WString literalWString = Wt::WString("Literal WString");
std::string str = "std::string";

// Will not change.
layout->addWidget(std::make_unique<Wt::WText>(localWString + str));

// Will not change.
layout->addWidget(std::make_unique<Wt::WText>(localWString + literalWString));

// Will not change.
layout->addWidget(std::make_unique<Wt::WText>(localWString + localWString));

// Will change.
layout->addWidget(std::make_unique<Wt::WText>(localWString));

// localWString becomes literal due to += operator
localWString += literalWString;

// Will not change
layout->addWidget(std::make_unique<Wt::WText>(localWString));
ConcatenationExamples_baseText (messages.xml)
  <!-- This is also defined in messages_nl.xml and messages_fr.xml -->
  <message id="ConcatenationExamples_baseText">
    This is or was a localized WString
  </message>

In the example, you can see that even though localWString becomes literal after the += operator, it does not impact the widgets where it was previously used. This is because the widgets keeps a copy of the WString which stays localized.

3.1.1. Formatting WStrings

Even though concatenating will always result in a literal WString, formatting won’t.

In a WString, you can use {1}, {2}, etc. as placeholders for other values. The first time WString::arg() will be called the placeholders {1} in the WString, will be replaced by the value given as argument. The second time it is called, the same will happen for the placeholder {2}, and so on.

For a localized WString, the replacement of the placeholders is deferred, as the value of the WString is not yet known. Instead, the replacement is done every time the value of the WString is needed. Because of this, a localized WString replacing a placeholder in another localized WString will correctly update when the locale changes. This does however not works when formatting a literal WString.

FormatExample.C
Wt::WString localWString = Wt::WString::tr("FormatExample_baseText");

Wt::WString literalWString = Wt::WString("Literal WString");

std::string str = "std::string";

// We need to copy localWString before calling arg() because it modifies the WString.

layout->addWidget(std::make_unique<Wt::WText>
    (Wt::WString(localWString).arg(str)));

layout->addWidget(std::make_unique<Wt::WText>
    (Wt::WString(localWString).arg(literalWString)));

layout->addWidget(std::make_unique<Wt::WText>
    (Wt::WString(localWString).arg(
      // We remove the placeholder by passing an empty string.
      Wt::WString(localWString).arg(""))));

layout->addWidget(std::make_unique<Wt::WText>(localWString));

Wt::WString literalWString2 = literalWString + " {1}";

// This will not change update with the new locale because it is a literal WString.
layout->addWidget(std::make_unique<Wt::WText>(literalWString2.arg(
  // We remove the placeholder by passing an empty string.
  Wt::WString(localWString).arg(""))));

// literalWString2 is now modified.
layout->addWidget(std::make_unique<Wt::WText>(literalWString2));
FormatExample_baseText (messages.xml)
  <!-- This is also defined in messages_nl.xml and messages_fr.xml -->
  <message id="FormatExample_baseText">
    This is or was a localized WString {1}
  </message>

Formatting a WString does not creates a new one. This is shown with the last WText in the example, and is the reason why we are copying localWString before formatting it.

3.1.2. Refreshing values

While formatting helps preserve localization, it does not solve the problem completely. Sometimes, you may be forced to use literal strings, like when creating a WLink. In that case, you will need to manually refresh the value of the string when the locale changes.

When the string is inside a WWidget, you should override the refresh() method. This method is called on all WWidget in the DOM tree after the WLocale is changed. By overriding it, you can replace the string with the appropriate value for the new locale.

Always call the base class’s refresh() method within your overridden refresh() function. Not doing so can prevent the widget from being refreshed correctly.

RefreshExamples.C
RefreshExamples::RefreshExamples()
{
  auto layout = setLayout(std::make_unique<Wt::WVBoxLayout>());

  localWString_ = Wt::WString::tr("ConcatenationExamples_baseText");

  literalWString_ = Wt::WString("Literal WString");

  str_ = "std::string";

  concatStr_ = layout->addWidget(std::make_unique<Wt::WText>(localWString_ + str_));
  concatLiteral_ = layout->addWidget(std::make_unique<Wt::WText>(localWString_ + literalWString_));
  concatLocal_ = layout->addWidget(std::make_unique<Wt::WText>(localWString_ + localWString_));

  layout->addWidget(std::make_unique<Wt::WText>(localWString_));
}

void RefreshExamples::refresh()
{
  concatStr_->setText(localWString_ + str_);
  concatLiteral_->setText(localWString_ + literalWString_);
  concatLocal_->setText(localWString_ + localWString_);

  Wt::WContainerWidget::refresh();
}

4. WTemplate

A WTemplate is a WWidget that renders a XHTML template.

4.1. Variables

Within a WTemplate, you can use variables as placeholders for dynamic content. These placeholders are replaced with actual values when the template is rendered. These variables, denoted by ${variable-name}, are resolved each time the WTemplate is updated.

Variable names can contain alphanumeric characters, hyphens (-), underscores (_), and periods (.).

Main-Page (templates.xml)
<message id="Main-Page">
  <div style="position:sticky; top:0px; background-color:LightGray; padding:10px;">
    Change examples language: ${language-btn}
  </div>
  <div style="padding:10px;">
    <h2>Intro</h2>
    <p>${intro}</p>
    ${block:Element LocalStr}
    ${block:Element Concat}
    ${block:Element Refresh}
    ${block:Element Format}
  </div>
</message>

You can bind values to variables using bindString(), bindInt(), or bindWidget() for strings, integers, and widgets, respectively.

For widgets specifically the bindNew<Widget>() functionality also exists. This allows for widgets to be added, and constructed at the same time. The arguments it takes are the constructor arguments for the Widget class.

Binding values to template variables (localization.C)
Wt::WTemplate *t = root()->addWidget(
  std::make_unique<Wt::WTemplate>(Wt::WString::tr("Main-Page")));

// Binding strings and widgets to the variables in the template
t->bindString("intro", Wt::WString::tr("intro"));
t->bindWidget("language-btn", std::make_unique<LanguageButton>());

While it is possible to provide styling directly in the template, it is recommended to use a separate CSS file.

4.2. Functions

WTemplate also supports functions, which allow you to perform more complex operations within your templates. Functions are placeholders that are replaced using a C++ function.

Functions are represented by ${function-name:argument1 argument2 ...}. Arguments are separated by spaces. In case an argument needs to contain spaces, it is possible to add them, by surrounding the the whole argument by single quotes(').

Before a function can be used in a template, it must be registered using WTemplate::addFunction(). This method takes two arguments:

  1. The function name (a string).

  2. A function pointer with the signature bool(WTemplate*, const std::vector<WString>&, std::ostream&).

Wt comes with several built-in functions:

  • id: Replaces the placeholder with the ID of the widget bound to the given variable.

  • tr: Replaces the placeholder with a localized WString using the given variable as the message ID.

  • block: Replaces the placeholder with a formatted, localized WString. The first argument is the message ID, and subsequent arguments are used for string formatting.

addFunction (localization.C)
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
t->addFunction("block", &Wt::WTemplate::Functions::block);

// Adding the examples
t->bindWidget("LocalStr-example", std::make_unique<LocalStringExample>());
t->bindWidget("Concat-example", std::make_unique<ConcatenationExamples>());
t->bindWidget("Refresh-example", std::make_unique<RefreshExamples>());
t->bindWidget("Format-example", std::make_unique<FormatExample>());
Element (templates.xml)
<message id="Element">
  <![CDATA[
  <div>
    <h2 id="{1}"> ${tr:{1}.title} </h2>
    ${block:{1}.description}
    ${block:Example {1}}
  </div>
  ]]>
</message>

<message id="Example">
  <![CDATA[
  <h4>Example:</h4>
  <p style="color:CornflowerBlue;">
    <i>See ${tr:{1}.file}</i>
  </p>
  <div style="border:1px solid Black; background-color:WhiteSmoke;">
    ${{1}-example}
  </div>
  ]]>
</message>

The block function first formats the WString (resolving any embedded variables like {1}), then renders the resulting string as a template. This allows for nested template-like behavior. For example, in the Element template, {1} will be replaced with the element’s name, and then the tr function will be called to replace the placeholder with the correct title.

Template placeholders cannot directly contain other template placeholders. This means that you cannot, for example, have ${function1:${function2:arg}}.

4.3. Why use WTemplate

WTemplate are very useful for building complex layouts in Wt. It’s ideal when you have a fixed number of widgets and need precise control over their placement. It allows a clean separation between presentation and application logic, improving code readability making it easier to maintain. It also allows to change the layout on the fly without restarting your server since the XHTML can be given by a localized WString. This also let designers tweak the look without needing C++ skills.

However, WTemplate isn’t the best choice for dynamic layouts. If you’re dealing with a variable number of widgets, or if their positions might change, you’ll likely want to use a WContainerWidget instead.

Alternatively, you may wish to look at a theme that will do content positioning for you, if you provide the right style classes or data elements (e.g. Bootstrap Grid).