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
#frand their slug doesn't have a hash, likefr. - Assign these internal tags to their corresponding posts AND pages.
- Set the HTML lang attribute to
<html lang="{{{block "lang"}}}">indefault.hbsfile. - Provide the content of the
langblock, especially inpost.hbsandpage.hbsfiles.
{{#post}}
{{#has tag="#en"}}
{{#contentFor "lang"}}fr{{/contentFor}}
{{else}}
{{#contentFor "lang"}}fr{{/contentFor}}
{{/has}}
{{!-- Rest of the post below !--}}
{{!-- ... !--}}
{{/post}}lang block of a post based on the language tag assigned to it.- Separate posts into two or more collections in the
routes.yamlfile.
collections:
/blog/:
filter: "tag:en"
permalink: /{slug}/
template: index-en
/blogue/:
filter: "tag:-en"
permalink: /{slug}/
template: index-frroutes.yaml file contains two collections: one for posts written in English and another for posts written in French.- Create the
index-fr.hbsandindex-en.hbstemplate files.
{{!< index}}
{{#contentFor "lang"}}fr{{/contentFor}}index-fr.hbs file.{{!< index}}
{{#contentFor "lang"}}en{{/contentFor}}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. ๐คจ


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"
}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.

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!
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);
};
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);
};
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:
- Include all localized strings inside the
locales/zz.jsonfile. - Change your publication language to
zz. - Edit the {{t}} helper file.

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! ๐






