Adding internationalization "i18n" to your posts in Ghost
In this post I explain in detail my experience and method which I had to implement to be able to write posts in English and Spanish using Ghost.
Table of contents
- Post's story
- Installing Ghost
- Setting down a Swap memory
- Installing Ghost's npm dependencies
- Setting up Ghost
- Installing Nginx
- Creating and setting up other Ghost instance for additional language
- Modifying Nginx configuration file
- Turning on both Ghost instances
- Changing default post's route in our Spanish Ghost instance
Post's story
When you are a bilingual person and you want to begin in the blogging world using Ghost, you could come up with the next question: ¿How can I write posts on several languages using this great open source tool which is Ghost?
That was the question which I asked myself when I was starting up this blog due to I wanted to leverage the different audiences who exist in both languages on Spanish as well as on English in my professional area which is Software Engineering, and also these audiences were benefited from my high-value content that I was going to share.
The first thing I did like a good self-taught person was to seek in Google:
- ¿How could I add i18n to Ghost?
- ¿How could I write a post in several languages using Ghost?
But ... the results weren't what I was expecting, I found this Github issue [Epic] Ghost and i18n it was open since 2014 ... just looking at the date when it was opened made me feel a bad feeling, however I kept reading comment after comment of that issue and I realized that many Ghost community members were discussing different ways to add i18n to Ghost, especially what tools to use, what patterns to follow and so forth.
Finally I realized that issue/epic was for adding support to many languages in Ghost's admin, not for overall posts.
As last resource I decided to join to Ghost's slack and ask people which have more time and experience working with Ghost than me. The answer was that still there wasn't an official support for adding i18n to our posts written in Ghost. So I decided to take necessary actions to resolve this issue and make this possible.
Creating a plugin from scratch was going to take a lot of time, because I had to analyze in a certain low level the way Ghost was built and then based on it develop the plugin, but quickly I discarded that option because of the factor time.
The second option I came up with was the one which I implemented and is the next: Create two Ghost instances in different ports and with Nginx proxies redirecting to one instance or another, then create a key/value object to save both Ghost instances URLs and adding a button with the option to change the current language.
I'm going to explain you at a very detailed level the process to achieve this second i18n option, for this sample I've used a DigitalOcean container specifically the standard 5$ monthly one on top of Ubuntu 16.04 64 bits and NodeJs v6.9.2.
Installing Ghost
We'll have to install zip
and wget
packages which we'll use to download Ghost, to install Ghost is recommended to place it in /var/www/ghost
route, so let's first create /var/www/
directory where we'll download Ghost last version from its Github repository:
sudo apt-get update
sudo apt-get install zip wget
sudo mkdir -p /var/www/
cd /var/www/
sudo wget https://ghost.org/zip/ghost-latest.zip
Now that we have Ghost last version, we have to decompress it and also change our directory to /var/www/ghost/
.
sudo unzip -d ghost ghost-latest.zip
cd ghost/
Setting down a Swap memory
This step is only for those who have selected the 512-RAM DigitalOcean container, for those who are not following this post with that container are free to skip this step and go ahead to: Installing Ghost's npm dependencies
Before installing Ghost's npm dependencies we have to enable Swap memory, we do this to avoid the operating system running out of memory and stops the npm install process.
Swap is an area on a hard drive that has been designated as a place where the operating system can temporarily store data that it can no longer hold in RAM. Basically, this gives us the ability to increase the amount of information that our server can keep in its working "memory", with some caveats. The space on the hard drive will be used mainly when space in RAM is no longer sufficient for data.
Creating a Swap file
We will create a file called swapfile in our root (/) directory. The file must allocate the amount of space we want for our swap file.
We can create a 2 Gigabyte file by typing:
sudo fallocate -l 2G /swapfile
The prompt will be returned to you almost immediately. We can verify that the correct amount of space was reserved by typing:
ls -lh /swapfile
The previous command should have printed something like this:
-rw-r--r-- 1 root root 2.0G Feb 26 17:52 /swapfile
Enabling the swap file
Right now, our file is created, but our system does not know that this is supposed to be used for swap. We need to tell our system to format this file as swap and then enable it. Before we do that though, we need to adjust the permissions on our file so that it isn't readable by anyone besides root. Allowing other users to read or write to this file would be a huge security risk.
We can lock down the permissions by typing:
sudo chmod 600 /swapfile
Verify that the file has the correct permissions by typing:
ls -lh /swapfile
The previous command should have printed something like this:
-rw------- 1 root root 2.0G Feb 26 17:52 /swapfile
As you can see, only the columns for the root user have the read and write flags enabled.
Now that our file is more secure, we can tell our system to set up the swap space by typing:
sudo mkswap /swapfile
Our file is now ready to be used as a swap space. We can enable this by typing:
sudo swapon /swapfile
We can verify that the procedure was successful by checking whether our system reports swap space now:
sudo swapon -s
Our swap has been set up successfully and our operating system will begin to use it as necessary.
Making our swap file permanent
We have our swap file enabled, but when we reboot, the server will not automatically enable the file. We can change that though by modifying the fstab
file.
Let's edit the file with root privileges in our text editor:
sudo nano /etc/fstab
At the bottom of the file, we need to add a line that will tell the operating system to automatically use the file we created:
/swapfile none swap sw 0 0
Save and close the file when we finished.
Installing Ghost's npm dependencies
Now we can install the Ghost dependencies and node modules (production dependencies only):
sudo npm install --production
Ghost is installed when this completes. We need to set up Ghost before we can start it.
Setting up Ghost
Ghost's configuration file should be located at /var/www/ghost/config.js
. However, no such file is installed with Ghost. Instead, the installation includes config.example.js
, so let's copy the example configuration file to the proper location.
Be sure to copy instead of move so we have a copy of the original configuration file in case we need to revert our changes.
sudo cp config.example.js config.js
Open the file for editing:
sudo nano config.js
You have to change the value of url to whatever your domain is (or you could use your server's IP address in case you don't want to use a domain right now). This value must be in the form of an URL. For example, http://example.com/
or http://45.55.76.126/
. If this value is not formatted correctly, Ghost will not start.
Also change the value of host
in the server section to 0.0.0.0
.
Save the file and exit the nano text editor by pressing CTRL+X
then Y
and finally ENTER.
Installing Nginx
The next step is to install Nginx. Basically, it will allow connections on port 80
to connect through to the port that Ghost is running on. In simple words, you would be able to access your Ghost blog without adding the :2368. Then we'll add the corresponding proxies for each language.
Let's install it with the following command:
sudo apt-get install nginx
Next, we will have to configure Nginx by changing our directory to /etc/nginx
and removing the default file in /etc/nginx/sites-enabled
:
cd /etc/nginx/
sudo rm sites-enabled/default
We will create a new file in /etc/nginx/sites-available/
called ghost
and open it with nano
to edit it:
sudo touch /etc/nginx/sites-available/ghost
sudo nano /etc/nginx/sites-available/ghost
Paste the following code in the file and change the server name to your domain name, or your servers IP address if you don't want to add a domain now:
server {
listen 80;
server_name your_domain_name.com;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For
$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:2368;
}
}
Save the file and exit the nano text editor by pressing CTRL+X
then Y
and finally ENTER.
We will now symlink our configuration in sites-enabled
:
sudo ln -s /etc/nginx/sites-available/ghost /etc/nginx/sites-enabled/ghost
We will restart Nginx:
sudo service nginx restart
Now we would need to start Ghost:
cd /var/www/ghost
npm start --production
You should be able to access your blog on port 80
as http://your_server_ip/
or http://your_domain_name/
.
Creating and setting up other Ghost instance for additional language
In case you have Ghost running, press Ctrl + C
or Command + C
to stop it, Now we'll create other Ghost instance, to do this, let's change our directory to /var/www/ghost
and once there we'll create a new folder called en
and we move all our Ghost content into it:
mkdir en
mv * en
Let's create another folder called es
and we'll copy all content of our folder en
into es
:
mkdir es
cp -r en/* es/
We'll use our en
folder for English language and es
folder for Spanish. We need to modify some files into our folder es
for adding the multi language support, first thing we'll modify is Ghost configuration file:
cd es/
sudo nano config.js
We have to modify the url
property into production object, as well as the server
property, in url
property we'll add http://your_server_ip/es/blog
and in the server
property we'll modify the port
property changing it to 2369
. We do this aiming that our URLs of each Ghost instance follow a SEO good practice which is loading its content identified by country's name by the standard ISO 3166-1
, as well as when starting up both instances each one run in a different port.
Save the file and exit the nano text editor by pressing CTRL+X
then Y
and finally ENTER.
Our production object should look like this:
production: {
url: 'http://your_server_ip/es/blog',
mail: {},
database: {
client: 'sqlite3',
connection: {
filename: path.join(__dirname, '/content/data/ghost.db')
},
debug: false
},
server: {
host: '0.0.0.0',
port: '2369'
}
}
We have to do the same previous modification into our en
folder for English language:
Directory en/config.js
production: {
url: 'http://your_server_ip/en/blog',
mail: {},
database: {
client: 'sqlite3',
connection: {
filename: path.join(__dirname, '/content/data/ghost.db')
},
debug: false
},
server: {
host: '0.0.0.0',
port: '2368'
}
}
We'll add the HTML necessary part to show a button to change user current language, we have to move into casper
folder which is Ghost's default theme content/themes/casper/
:
cd content/themes/casper/
We'll add the following HTML code just below of our top right Menu button into <nav></nav>
tag in index.hbs
, post.hbs
, tag.hbs
and author.hbs
files, we'll do this modifications in es
folder for Spanish as well as in en
folder for English:
Spanish version - Directory es/content/themes/casper/
<dl class="i18n">
<dt><a href="javascript:void(0);"><span>Idioma</span></a></dt>
<dd>
<ul>
<li><a href="{{url}}">Español</a></li>
<li><a href="#">Ingles</a></li>
</ul>
</dd>
</dl>
English version - Directory en/content/themes/casper/
<dl class="i18n">
<dt><a href="javascript:void(0);"><span>Language</span></a></dt>
<dd>
<ul>
<li><a href="{{url}}">English</a></li>
<li><a href="#">Spanish</a></li>
</ul>
</dd>
</dl>
Nav section in those files should look like this:
Spanish version
<nav class="main-nav overlay clearfix">
{{#if @blog.logo}}<a class="blog-logo" href="{{@blog.url}}"><img src="{{@blog.logo}}" alt="{{@blog.title}}" /></a>{{/if}}
{{#if @blog.navigation}}
<a class="menu-button icon-menu" href="#"><span class="word">Menu</span></a>
{{/if}}
<dl class="i18n">
<dt><a href="javascript:void(0);"><span>Idioma</span></a></dt>
<dd>
<ul>
<li><a href="{{url}}">Español</a></li>
<li><a href="#">Ingles</a></li>
</ul>
</dd>
</dl>
</nav>
English version
<nav class="main-nav overlay clearfix">
{{#if @blog.logo}}<a class="blog-logo" href="{{@blog.url}}"><img src="{{@blog.logo}}" alt="{{@blog.title}}" /></a>{{/if}}
{{#if @blog.navigation}}
<a class="menu-button icon-menu" href="#"><span class="word">Menu</span></a>
{{/if}}
<dl class="i18n">
<dt><a href="javascript:void(0);"><span>Language</span></a></dt>
<dd>
<ul>
<li><a href="{{url}}">English</a></li>
<li><a href="#">Spanish</a></li>
</ul>
</dd>
</dl>
</nav>
Next step is for adding corresponding styles to properly show the change language button in desktop computers, tablets as well as in smartphones.
To do this let's open the screen.css
files located in es/content/themes/casper/assets/css/screen.css
and en/content/themes/casper/assets/css/screen.css
we'll add the next style code just below the animations section:
/* ===============================================================
15. i18n styles
=============================================================== */
dl.i18n, .i18n dd, .i18n dt {
margin:0px;
padding:0px;
width:100px;
border-radius: 3px;
}
dl.i18n {
margin-right:10px;
}
.i18n dd {
position:relative;
}
.i18n dt a {
color: #fff;
height:36px;
text-decoration: none;
font-family: 'Open Sans', sans-serif;
line-height: 1.75em;
background:transparent;
display:block;
font-size: 1.5rem;
border:1px solid #BFC8CD;
text-align: center;
font-weight: normal;
}
.i18n dt a span {
display:block;
padding:5px;
}
.i18n dd ul {
box-shadow: 1px 1px 4px #9EABB3;
border-radius:3px;
background:rgb(245, 248, 250) none repeat scroll 0 0;
display:none;
font-size: 1.5rem;
list-style:none;
padding:0px;
position:absolute;
left:0px;
min-width:100px;
z-index: 200;
}
.i18n dd ul li:first-child {
box-shadow: 0px 1px 0px #9EABB3;
}
.i18n span.value {
display:none;
}
.i18n dd ul li a {
padding:5px;
display:block;
}
@media (max-width: 500px) {
.i18n dt a {
border:none;
}
}
@media (max-width: 960px) {
dl.i18n {
float:left;
}
.main-header {
overflow:auto;
}
}
@media (min-width: 960px) {
dl.i18n {
float:right;
}
.main-header {
overflow:auto;
}
}
We already have the styles and the button, the only missing thing would be adding the functionality and it is what we're going to do, we'll need to modify the next files default.hbs
which is located in content/themes/casper/default.hbs
and index.js
located in content/themes/casper/assets/js/index.js
.
The first file we'll modify is default.hbs
where we're going to add just before </body>
tag and after script tags:
Spanish version - es/content/themes/casper/default.hbs
{{!-- Script for i18n --}}
<script type="text/javascript">
$(document).ready(function () {
var i18nEnglishUrl = window.location.href.replace('{{url}}', i18nEnglishKeys['{{url}}']);
$('.i18n dd ul li > a[href="#"]').attr("href", i18nEnglishUrl);
});
</script>
English version - en/content/themes/casper/default.hbs
{{!-- Script for i18n --}}
<script type="text/javascript">
$(document).ready(function () {
var i18nEnglishUrl = window.location.href.replace('{{url}}', i18nSpanishKeys['{{url}}']);
$('.i18n dd ul li > a[href="#"]').attr("href", i18nEnglishUrl);
});
</script>
Also we'll add just at the very beginning of index.js
file:
Spanish version - es/content/themes/casper/assets/js/index.js
// i18n keys
var i18nEnglishKeys = {
'/es/blog/': '/en/blog/',
'/es/blog/bienvenido-a-ghost/': '/en/blog/welcome-to-ghost/'
};
$(document).ready(function() {
// i18n button
$(".i18n dt a").click(function() {
$(".i18n dd ul").toggle();
});
$(document).bind('click', function(e) {
var $clicked = $(e.target);
if (! $clicked.parents().hasClass("i18n"))
$(".i18n dd ul").hide();
});
});
English version - en/content/themes/casper/assets/js/index.js
// i18n keys
var i18nSpanishKeys = {
'/en/blog/': '/es/blog/',
'/en/blog/welcome-to-ghost/': '/es/blog/bienvenido-a-ghost/'
};
$(document).ready(function() {
// i18n button
$(".i18n dt a").click(function() {
$(".i18n dd ul").toggle();
});
$(document).bind('click', function(e) {
var $clicked = $(e.target);
if (! $clicked.parents().hasClass("i18n"))
$(".i18n dd ul").hide();
});
});
Modifying Nginx configuration file
We'll modify the file we created in Installing Nginx step, we do this to get our server pointing out to our 2 Ghost instances:
sudo nano /etc/nginx/sites-available/ghost
Let's delete all its content and paste the next one:
server {
listen 80;
server_name localhost;
location / {
rewrite ^ http://my_domain_name_or_server_ip/en/blog/ permanent;
}
location /en/blog/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://localhost:2368;
}
location /es/blog/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://localhost:2369;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
Be aware that you should change my_domain_name_or_server_ip
to your page's domain name or server ip in which Ghost is installed.
Save the file and exit the nano text editor by pressing CTRL+X
then Y
and finally ENTER.
We have to execute the next command to be able to see our changes reflected in Nginx:
sudo service nginx restart
Turning on both Ghost instances
Let's move into our both Ghost instances to turn them on:
cd /var/www/ghost/en/
npm start --production &
cd /var/www/ghost/es/
npm start --production &
That way we have our Spanish and English Ghost instances running in our server.
Changing default post's route in our Spanish Ghost instance
Let's open our preferred web navigator and go to Ghost instance route which we have defined:
http://my_domain_name_or_server_ip/es/blog/
We'll successfully see our Spanish instance is properly working...
Now let's move in to our administration section "admin", it'd be:
http://my_domain_name_or_server_ip/es/blog/admin
Once there we'll see Ghost's initial settings to set down our access credentials, fill out those fields and continue to Ghost's dashboard.
Let's click in Ghost default post "Welcome to Ghost", and then click in settings icon located at the top right corner, it'll allow us to change our first post URL. Change the value in "Post URL" field to bienvenido-a-ghost
.
This way we have successfully set down our first post which is working with i18n :)
As we go creating more posts, we have to register its respective URLs in the objects which contain our URLs for both languages. This way our i18n functionality will have a map with URLs available and also it'll know which one to use depending of current active URL.
For example, let's assume that this post you are reading is new and as URL we have assigned it adding-internationalization-i18n-to-ghosts-posts
for English and agregando-internacionalizacion-i18n-a-los-posts-en-ghost
for Spanish, then we have to add it into our URLs map we already have:
Directory - es/content/themes/casper/assets/js/index.js
// i18n keys
var i18nEnglishKeys = {
'/es/blog/': '/en/blog/',
'/es/blog/bienvenido-a-ghost/': '/en/blog/welcome-to-ghost/',
'agregando-internacionalizacion-i18n-a-los-posts-en-ghost': 'adding-internationalization-i18n-to-ghosts-posts'
};
Directory - en/content/themes/casper/assets/js/index.js
// i18n keys
var i18nSpanishKeys = {
'/en/blog/': '/es/blog/',
'/en/blog/welcome-to-ghost/': '/es/blog/bienvenido-a-ghost/',
'adding-internationalization-i18n-to-ghosts-posts': 'agregando-internacionalizacion-i18n-a-los-posts-en-ghost'
};
With that we'd have registered our 2 new URLs for both languages and instances.
That'd be all for this post, I hope you find this useful to be able to have your Ghost posts in different languages, any doubt don't hesitate to ask me through the comments, as well as additional consults, opinions or overall things you think I should improve.
Greetings and asynchronous hugs.