Building a static multi-language site with Metalsmith (part II)
| Tags: front-end
This is the second part of the Building a static multi-language site with Metalsmith series. If you are already familiar with Metalsmith or need more context, please read part I.
You can also fetch the source code from the Github repository.
Creating content with multiple languages
Now we will start with multi-language content. We will be usign the metalsmith-multi-language
plugin which does a bunch of useful stuff for us:
-
Provided files with the same name but a different suffix with a locale, it will recognize them as "translations" of the same content. That is, if we have
paella_es.md
,paella_fr.md
andpaella_en.md
it will allow us to make reference to the other locale variations from each file. We will use this feature to include a link in every page to the other locales. -
It will add a
locale
metadata field to every file, based on its suffix. -
It will merge the metadata of the main locale into the other files, so we don't need to repeat metadata that stays the same across locales.
-
It will take all
index_*
files and build them into a subdirectory with its locale. That is, if we haveindex_es.html
,index_fr.html
andindex_en.html
and we build the site, it will yield the following (assumingen
is our main locale):
├── es
│ └── index.html
├── fr
│ └── index.html
└── index.html
In my own case, I just needed two locales: English (en
) and Spanish (es
). Let's replicate that. Rename index.md
to index_en.md
and add a index_es.md
as well:
---
title: Inicio
---
Mi colección de **recetas**.
Use the plugin before doing any other processing:
var multiLanguage = require('metalsmith-multi-language');
const DEFAULT_LOCALE = 'en';
const LOCALES = ['en', 'es'];
metalsmith(__dirname)
// ...
.destination('dist')
.use(
multiLanguage({
default: DEFAULT_LOCALE,
locales: LOCALES,
})
);
// ...
This is how the built site looks like:
dist
├── css
│ └── styles.css
├── es
│ └── index.html
└── index.html
And if we open es/index.html
, we can see the content in Spanish:
Let's put a link in each file to their translation counterpart. We can use the lang
method that is added to every file. Given a locale, it will match that version. Unfortunately, the plugin we are using doesn't inject the list of available locales for every file, so we need to calculate that ourselves.
How? The plugin does add the locales
and defaultLocale
fields that we specified when using the plugin. With that, we just need to filter out the current locale from the locales
array and the locales with no translation yet. We can get that by querying lang
: if it returns undefined
, that file isn't available in that locale.
Since we'll want this in every page of our site, we will add this to the default.jade
layout:
main: article
- var availableLocales = locales.filter(x => x !== locale && !!lang(x));
if availableLocales
p Also available in#{' '}
each lc, index in availableLocales
a(href=`/${lang(lc).path}`)= lc
if index < availableLocales.length - 1
|,#{' '}
|.
If you build the site, you will see the link in action. Note that since the link URL is absolute, you will need to run a local server:
In addition to the link to the translation, it would be good to include a link to the index
of each locale in the main heading. We just need to take into account that the main locale has a different pattern than the others.
Let's add this link to default.jade
:
header
- var url = locale === defaultLocale ? '/' : `/${locale}/`;
h1: a(href=url)= __('Cookbook')
Adding the locale to the URL
We want to replicate what the metalsmith-multi-language
plugin does with the index
files with the rest of our content (why isn't this feature included in the plugin itself?).
In our case, we will put every translation of a file to their own locale subdirectory –including the main locale as well. So for our cookbook example, we would have es/recipes/paella.html
and en/recipes/paella.html
.
We can achieve this with yet another plugin: metalsmith-permalinks
. It will also allow us to have nice URL's, so we could access /en/recipes/paella/
instead of /en/recipes/paella.html
.
Create a content/recipes/
directory and add two files to it.
This is the English version, content/recipes/tortilla_en.md
:
---
title: Spanish omelette
---
Ingredients:
- 2 potatoes
- 3 eggs
- 1 onion
- Olive oil
- Salt
And this is its Spanish counterpart, content/recipes/tortilla_es.md
:
---
title: Tortilla de patata
---
Ingredientes:
- 2 patatas
- 3 huevos
- 1 cebolla
- Aceite de oliva
- Sal
When using the metalsmith-permalinks
plugin, we will disable the relative
setting. By default, the plugin allows relative URL's by duplicating the files all over. This is generally useful, but since we could potentially have multiple locales (and thus, multiple siblings), it seems a waste of space.
We will also be using the file's title
to create the URL. In that way, we have more SEO-friendly URL's:
var permalinks = require('metalsmith-permalinks');
metalsmith(__dirname
// ...
.use(markdown())
.use(permalinks({
relative: false,
pattern: ':locale/:title/'
}))
// ...
After building the site, you will see the following file tree:
dist
├── css
│ └── styles.css
├── en
│ ├── home
│ │ └── index.html
│ └── spanish-omelette
│ └── index.html
└── es
├── inicio
│ └── index.html
└── tortilla-de-patatas
└── index.html
Everything looks kind of right, but we don't want our index
files to be handled by permalinks. In order to disable it for these files, add a permalink: false
to their metadata. For instance:
---
title: Home
permalink: false
---
Note that you only need to add that piece of metadata to the index of the main locale (in this case, index_en.md
). This is because the multi-language plugin merges the metadata from the main locale to the other ones. Once you have edited that file, you will have the structure we want:
dist
├── css
│ └── styles.css
├── en
│ └── spanish-omelette
│ └── index.html
├── es
│ ├── index.html
│ └── tortilla-de-patatas
│ └── index.html
└── index.html
Translating the layout
You already noticed that although our Markdown content is translated, the site itself isn't. There is a plugin that brings us the i18n library, metalsmith-i18n
. With this library, we will mark strings to be translated. We will have a JSON file per locale, ready for us to translate, and, at the same time, those strings marked will be translated if its translation is available.
npm install --save metalsmith-i18n
As with the multi-language plugin, we need to specify the default locale and the available ones. We also need to indicate where do we want the translation JSON files to be generated.
var i18n = require('metalsmith-i18n');
metalsmith(__dirname)
// ...
.use(i18n({
default: DEFAULT_LOCALE,
locales: LOCALES,
directory: 'locales'
}))
.use(markdown()
// ...
Running it will change nothing in our site… but it will add a locales
directory to the root of the project. If you inspect the JSON files in it, they both will contain an empty object.
Let's mark some strings for translation in layouts/default.jade
. We will do this by calling the __()
function.
doctype html
head
title #{__('Cookbook')} - #{title}
meta(charset='utf-8')
link(rel='stylesheet', href='/css/styles.css')
body
header
h1= __('Cookbook')
main: article
- var availableLocales = locales.filter(x => x !== locale && !!lang(x));
if availableLocales
p #{__('Also available in') + ' '}
each lc, index in availableLocales
a(href=`/${lang(lc).path}`)= __(lc)
if index < availableLocales.length - 1
|,#{' '}
|.
h1= title
!= contents
Build the site again and you will see that the JSON files contain keys to translate. Let's do it. For English we just need to edit the language locale string for Spanish. Edit locales/en.json
:
{
"Cookbook": "Cookbook",
"Also available in": "Also available in",
"es": "castellano"
}
For locales/es.json
we need to do a bit of translation…
{
"Cookbook": "Recetario",
"Also available in": "Disponible también en",
"en": "English"
}
Re-build the site and, here it is!
Translating URL's
We are almost done. Something that would be nice is create more elaborated URL's. So instead having everything hanging from root, we could have the recipes inside their own translated prefix. For instance, /en/recipes/<title>/
and /es/recetas/<title>/
.
To do that we need to classify the recipes by locale and then having the permalinks plugin to run a different URL for each locale. We can do that by using the metalsmith-collections
. This plugin let us organise our files into collections matching a particular pattern. Later we can use these collections to create permalinks or to output a list of files, for instance.
npm install --save metalsmith-collections
We will group the recipes by their locale:
var collections = require('metalsmith-collections');
metalsmith(__dirname)
// ...
.destination('dist')
.use(
collections({
recipes_en: 'recipes/*_en.md',
recipes_es: 'recipes/*_es.md',
})
)
// ...
.use(
permalinks({
relative: false,
pattern: ':locale/:title/',
linksets: [
{
match: { collection: 'recipes_en' },
pattern: ':locale/recipes/:title/',
},
{
match: { collection: 'recipes_es' },
pattern: ':locale/recetas/:title/',
},
],
})
);
Run it and see it in action:
dist
├── css
│ └── styles.css
├── en
│ └── recipes
│ └── spanish-omelette
│ └── index.html
├── es
│ ├── index.html
│ └── recetas
│ └── tortilla-de-patatas
│ └── index.html
└── index.html
Getting files of a single locale
Finally, we need to create some sort of index for each locale that will output a list with links of its locale. This is useful for blogs –and for cookbooks too.
Collections are made available to templates inside a collections
object, so the recipes in Spanish would be available in collections['recipes_es']
.
What we will do is to create a new layout –that extends the default one– to be used by the index files, where this list of recipes will be available.
Edit layouts/default.jade
to add a new block which we can fill later via inheritance:
body
// ...
!= contents
block extra
Now create layouts/home.jade
:
extends ./default.jade
block extra
h2= __('Recipes')
ul
each recipe in collections[`recipes_${locale}`]
li: a(href=`/${recipe.path}`)= recipe.title
And that's it! Here's how the new index looks like:
I hope you found this useful. And don't forget that you can take a look at my actual cookbook as well!