How to Support Multilingual Pages in Ghost

Currently, a Ghost site can only be in one language. But with a few modifications, there is a way to have a website in multiple languages! ๐Ÿ˜ฎ

4 min read
How to Support Multilingual Pages in Ghost
Photo by Marina Shatskikh on Pexel.
๐Ÿ’ก
This article is the first in my series on building a multilingual website with Ghost.

Prerequisites

In this article, I assume that you have already followed the official guide to have multilingual content. If this is already the case, you can skip to the introductory section. If not, here's a quick summary below that you might want to follow.

  • Create an internal tag for each language you want to support. Make sure their name looks like #fr and their slug doesn't have a hash, like fr.
  • Assign these internal tags to their corresponding posts AND pages.
  • Set the HTML lang attribute to <html lang="{{{block "lang"}}}"> in default.hbs file.
  • Provide the content of the lang block, especially in post.hbs and page.hbs files.
{{#post}}
  {{#has tag="#en"}}
    {{#contentFor "lang"}}fr{{/contentFor}}
  {{else}}
    {{#contentFor "lang"}}fr{{/contentFor}}
  {{/has}}

  {{!-- Rest of the post below !--}}
  {{!-- ... !--}}
{{/post}}
Code to set the lang block of a post based on the language tag assigned to it.
  • Separate posts into two or more collections in the routes.yaml file.
collections:
  /blog/:
    filter: "tag:en"
    permalink: /{slug}/
    template: index-en
  /blogue/:
    filter: "tag:-en"
    permalink: /{slug}/
    template: index-fr
This routes.yaml file contains two collections: one for posts written in English and another for posts written in French.
  • Create the index-fr.hbs and index-en.hbs template files.
{{!< index}}
{{#contentFor "lang"}}fr{{/contentFor}}
The content of my index-fr.hbs file.
{{!< index}}
{{#contentFor "lang"}}en{{/contentFor}}
The content of my index-en.hbs file.

Introduction

After successfully completing the previous Ghost tutorial, you may quickly notice that the layout is still in the default language you configured in the backend. ๐Ÿคจ

Screenshot of the section where to change the publication language in Ghost (Settings โ†’ General).
Screenshot of a post written in French, with an English layout.

Well... that's strange, isn't it? ๐Ÿ˜ฃ

Even if you use the translate helper {{t}} to localize your strings, they are still in the publication language... They seem to be not aware of the lang context you've previously configured... But how can we fix this? ๐Ÿค”

Solution

If you look at Ghost's source code, you might see that the engine only loads in memory one language file at a time (ref). And our theme translations are currently in individual files in the locales folder...

One solution could be to refactor some parts of Ghost's code source to load all localization files, but I haven't tried that as it might require a big refactor. ๐Ÿ˜ฌ

What if we put all our localized strings inside one big file? To have a valid JSON file, all strings must be unique. They should therefore be prefixed or suffixed with the lang code.

{
  "en.Blog": "Blog",
  "fr.Blog": "Blogue"
}
An excerpt from my locales/zz.json file.

To make sure the file with all localisations wouldn't be confused with another language, I named the file with an unknown language named zz. I also changed Ghost's publication language.

The publication language is now set to zz.

Now that all localized strings are loaded in Ghost's memory, how do we tell Ghost that they are now prefixed with their language? ๐Ÿค”

Let's take a look at the source code of the {{t}} helper!

โš ๏ธ
The following code was copied from Ghost v5.12.0. If you have another version, make sure to use the proper file, or you may introduce unusual bugs such as Cannot read properties of undefined (reading 't')! ๐Ÿ˜‰

The latest version (of the main branch) can be found here. ๐Ÿ˜Ž
const {themeI18n} = require('../services/handlebars');

module.exports = function t(text, options) {
  const bindings = {};
  let prop;
  for (prop in options.hash) {
    if (Object.prototype.hasOwnProperty.call(options.hash, prop)) {
      bindings[prop] = options.hash[prop];
    }
  }

  return themeI18n.t(text, bindings);
};
The core/frontend/helpers/t.js file before our changes.

The helper currently has a mysterious options parameter, but what's in it? Could we find there the language we defined for the {{{block 'lang'}}}?

It turns out yes! The current language is in a deep location: options.data.root.blockCache.lang. Let's update the code to use it. ๐Ÿ˜

const {themeI18n} = require('../services/handlebars');

module.exports = function t(textObject, options) {
  const text = typeof textObject === "string" ? textObject : textObject.string;
  const lang = options.data.root.blockCache?.lang?.[0];
  const path = lang ? `${lang}.${text}` : text;

  const bindings = {};
  let prop;
  for (prop in options.hash) {
    if (Object.prototype.hasOwnProperty.call(options.hash, prop)) {
      bindings[prop] = options.hash[prop];
    }
  }

  return themeI18n.t(path, bindings);
};
The same core/frontend/helpers/t.js file after our changes.

If you restart your Ghost instance, all strings should now be localized! It's great, isn't it? ๐Ÿฅฐ

Conclusion

In summary, if you want to support multilanguage pages in Ghost, you should:

  1. Include all localized strings inside the locales/zz.json file.
  2. Change your publication language to zz.
  3. Edit the {{t}} helper file.
Notice how dates are now translated.

It's more pleasant for our visitors, right? All layouts are now in the same language as the post! A step forward towards a multilingual website! ๐Ÿ˜Ž