Adding internationalization "i18n" to your posts in Ghost

Adding internationalization "i18n" to your posts in Ghost
👋🏻
This post was written by Juan Delgadillo, an international software engineer, mentor, and traveler with more than a decade of experience helping worldwide startups and companies to design, build and deploy their web and mobile applications with a strong emphasis on added value, user experience, and efficiency.

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

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:

  1. ¿How could I add i18n to Ghost?
  2. ¿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.