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
- Defining Routes
- Defining Menus
- Making Templates and Fetching Content
- Implementing Multilingual Search
- Internationalization of Templates
- Implementing Multilingual Forms
- Limitations and Recommendations
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 thepages
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.
Implementing Multilingual Search¶
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.
Directly Link Individual Pages between Languages¶
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]
Couldn't find what you were looking for? We are happy to help you in the forum, on Slack or on Github.