Edit on GitHub
Jump to docs navigation

Howtos on various subjects / Building Multilingual Websites with Bolt

Note: You are currently reading the documentation for Bolt 2.2. Looking for the documentation for Bolt 5.2 instead?

Bolt does not support multilingual websites at the moment. There are often multiple ways to handle multilingual websites. This page describes one simple method to facilitate one.

In short, with this method you'll duplicate every ContentType per language (or region). So this will only work for sites with a few languages or without too many ContentTypes.

Note: This section requires some knowledge of Bolt and Twig (in particular, Template Inheritance). Please remember that this is only one way to handle multilingual content. Questions and/or suggestions are welcome, please check the contributing guide or the Bolt community page for more information.

Table of Contents

Defining ContentTypes

An important step when making websites, is to properly define your s . Since s are defined in YAML, there are some handy tricks you can apply. YAML provides node anchors (&) and references (*) for repeated nodes. So once the fields of a ContentType are defined, you can simply reference them. Be sure that the anchor is defined before it is used. See the use of &pagefields and *pagefields in the following example. Assume en is English, nl is Dutch, and de is German.

pages-en:
    name: Pages
    singular_name: Page
    fields: &pagefields
        title:
            type: text
            class: large
        slug:
            type: slug
            uses: title
        image:
            type: image
        text:
            type: html
            height: 300px
    template: page.twig

pages-nl:
    name: Paginas
    singular_name: Pagina
    fields: *pagefields
    template: page.twig

pages-de:
    name: Seiten
    singular_name: Seite
    fields: *pagefields
    template: page.twig

A recommended method is to use the same slugs for the same ContentTypes with the language as a prefix or postfix. You can choose to omit the language for the default ContentTypes if you desire:

postfix prefix
pages-en or pages en-pages or pages
pages-nl nl-pages
pages-de de-pages

Depending on the website and/or your preferences, you can group the definitions in contenttypes.yml by language or by ContentType:

by language by ContentType
en-pages pages-en
en-entries pages-nl
nl-pages entries-en
nl-entries entries-nl

See the following sections why it might be more useful to use en-pages and nl-pages instead of pages and paginas.

Defining Routes

A new route needs to be defined for every ContentType defined. This section will make use of the following patterns:

  • [language]/[contenttype]/[slug];
  • [language]/[slug], for the pages ContentType.

This makes the routes fairly straightforward to define:

# ------------------------------------------------------------------------------
# [en] English
# ------------------------------------------------------------------------------

en-entries:
  path:               /en/entry/{slug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::record', 'contenttypeslug': 'en-entries' }
  contenttype:        en-entries

# ... more contenttypes here ...

en-pages:
  path:               /en/{slug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::record', 'contenttypeslug': 'en-pages' }
  contenttype:        en-pages

# ------------------------------------------------------------------------------
# [nl] Dutch
# ------------------------------------------------------------------------------

nl-entries:
  path:               /nl/artikel/{slug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::record', 'contenttypeslug': 'nl-entries' }
  contenttype:        nl-entries

# ... more contenttypes here ...

nl-pages:
  path:               /nl/{slug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::record', 'contenttypeslug': 'nl-pages' }
  contenttype:        nl-pages

Tip: Make use of comments in your contenttypes.yml, routing.yml and menu.yml to divide different sections.

Defining Menus

Define your menus as usual. You'll need a duplicate of every menu per language. Be sure to prefix (or postfix) them, just like with ContentTypes and routes.

en-main:
  - label: Home
    path: en-pages/1
  - label: About
    path: en-pages/2

nl-main:
  - label: Home
    path: nl-pages/1
  - label: Over
    path: nl-pages/2

Tip: Don't forget to check the Menu Editor extension.

Making Templates and Fetching Content

Probably, the most interesting part. It is best to make use of the powerful Template Inheritance, in Twig, where you define one master template — e.g. master.twig — that is extended by other pages. Start by determining the current language based on the URL and define all ContentTypes and menus.

{% spaceless %}
{# --- attempt to get the language from the URL --- #}
{% if app.canonicalpath %}
    {% set languageslug = app.canonicalpath|split('/')[1] %}
{% else %}
    {% set languageslug = app.paths.current|split('/')[1] %}
{% endif %}

{# --- set the language, otherwise fallback to default language --- #}
{% if languageslug in ['en', 'nl', 'de'] %}
    {% set language = languageslug %}
{% else %}
    {% set language = 'en' %}
{% endif %}

{% set pagescontenttype      = language ~ '-pages' %}
{% set entriescontenttype    = language ~ '-entries' %}
{# ... more contenttypes ... #}

{% set menumain              = language ~ '-main' %}
{% set menufooter            = language ~ '-footer' %}
{# ... more menus ... #}

{% endspaceless %}

Now, in order to fetch content, you'll want to re-write all setcontent queries and menu() calls. Instead of:

{% setcontent pages = 'pages' where { ... } %}
{{ menu('main') }}

use:

{% setcontent pages = pagescontenttype where { ... } %}
{{ menu(menumain) }}

Basically, instead of directly calling setcontent with something directly like pages, save these language-dependent values in a variable.

Define a route for the search results pages for every language. Again, keep the same slugs for the routenames prefixed (or postfixed) with the language.

en-searchresults:
  path:               /en/searchresults
  defaults:           { _controller: 'Bolt\Controllers\Frontend::search' }

nl-searchresults:
  path:               /nl/zoekresultaten
  defaults:           { _controller: 'Bolt\Controllers\Frontend::search' }

In your template, use the following script to determine what the URL is for the search results page based on the current language. In your search form, set the action attribute to that URL.

{% set searchresultsurl = app.config.get('routing')[language ~ '-searchresults'].path %}

...

<form method="get" action="{{ searchresultsurl }}" id="search-form">
    ...
</form>

By default, the search_results_template is listing.twig. This can be modified in config.yml if desired. Every time a search is triggered, the variable records needs to be overridden. Define a variable that has all ContentTypes you want to search in:

{% if search %}
    {% allcontenttypes = [ pagescontenttype, entriescontenttype ]|join(',') %}
    {% setcontent records = '(' ~ allcontenttypes ~')/search' where { filter: search } %}
{% endif %}

Internationalization of Templates

If your templates has some strings that do not directly depend on content, you will want to translate these as well. The Labels extension is made for this purpose.

In your master.twig template, set the current language for Labels:

{{ setLanguage(language) }}

Now, instead of using text directly, you want to put them through the Labels function l(...) (lower-case L). This takes a string that you want to translate as an argument. Optionally, you can prefix this with a namespace. The syntax is <namespace>:<string>.

{{ l('The string you want to translate') }}
{{ l('namespace:The string you want to translate') }}

Note: The current implementation of the Labels extension does not allow the usage of colons (:) in translatable strings as this will define a namespace.

Note: The __() function is used internally by Bolt.

International Dates and Times

Usually, you want the dates and times in the same language. Currently, the default locale setting is set in config.yml. It depends on your server what locales are available. Use the following command:

locale -a

This will output a list of available locales. You'll probably see something like:

C
C.UTF-8
en_GB.utf8
en_US.utf8
nl_NL.utf8

In order to set the locale in a Twig template, you'll first need a mapping of languages to locales.

{% set locales =
  { 'en' : 'en_GB'
  , 'nl' : 'nl_NL'
  , 'de' : 'de_DE'
  }
%}

Set the correct locale and call the function initLocale to apply a new locale.

{% set ret = app.config.set('general/locale', locales[language]) %}
{{ app.initLocale() }}

When outputting dates, use the localdate filter. Note that this is only useful if the date structure is identical for every language, which is not always the case. You'll want to use a simple if statement for each exception.

{% if language == 'zh' %}
  {# -- Output a Chinese date -- #}
  {% set year  = record.datepublish|localdate("%Y") %}
  {% set month = record.datepublish|localdate("%m") %}
  {% set day   = record.datepublish|localdate("%d") %}
  {{year}}年{{month}}月{{day}}日
{% else %}
  {{ record.datepublish|localdate("%F") }}
{% endif %}

Implementing Multilingual Forms

[todo]

Limitations and Recommendations

This tutorial shows one of many ways to make a multilingual website. Since, this functionality is not provided out-of-the-box, there are some limitations with the aforementioned approach.

Twig vs Extension

This page provides many solutions by settings variables in Twig. This is not necessary the best way to do things, but it's workable in most situations. Many of these tricks can be ported into an extension to keep your templates clean(er). Think of setting the locale and exposing default variables via functions in a custom extension.

Tip: Use Twig macros to make reusable functions in Twig.

Boilerplate Master Template

Check out the boilerplate template that applies most of the abovementioned tricks to kickstart your theme for your multilingual site.

Multilingual Taxonomy Listings

If you need taxonomy listings per language, duplicate the taxonomies per language in taxonomy.yml. Then in contenttypes.yml, use the language-specific taxonomy in your ContentTypes.

If you want to link individual pages directly between languages, you will need to add a relationship per language and then manually link the contents.

en-pages:
  ...
  relations:
    nl-pages: &pagesrelationship
      multiple: false
      label: Select a page
      order: -id
    de-pages: *pagesrelationship

...

nl-pages:
  ...
  relations:
    en-pages: *pagesrelationship
    de-pages: *pagesrelationship

Output these links in your templates. Always add an additional check if an relationship is not defined.

{% set relatedrecords = record.related() %}

{% if relatedrecords['en-pages'] is not empty %}
    <a href="{{ relatedrecords['en-pages'].link }}">English</a>
{% else %}
    <a href="/en">English</a>
{% endif %}

{% if relatedrecords['nl-pages'] is not empty %}
    <a href="{{ relatedrecords['nl-pages'].link }}">Nederlands</a>
{% else %}
    <a href="/nl">Nederlands</a>
{% endif %}

Note: This approach is not recommended for sites with lots of content, since this is going to be a lot of work for editors. Furthermore, this only works if the website structure is exactly the same for every language.

Pagination on Search Results Pages

There currently is no pagination on search results pages.

[todo]


Edit this page on GitHub
Couldn't find what you were looking for? We are happy to help you in the forum, on Slack or on Github.