Agregando internacionalización "i18n" a tus posts en Ghost

Agregando internacionalización "i18n" a tus posts en Ghost
👋🏻
Este post fue escrito por Juan Delgadillo, un ingeniero de software internacional, mentor y viajero con más de una década de experiencia ayudando a startups y empresas de todo el mundo a diseñar, construir e implementar sus aplicaciones web y móviles con un fuerte énfasis en el valor agregado, la experiencia del usuario y la eficiencia.

En este post explico en detalle mi experiencia y el método que tuve que implementar para tener la opción de poder escribir posts con Ghost en varios idiomas, en mi caso Ingles y Español.


Tabla de contenidos


Historia de este post

Cuando se es bilingüe y también se quiere comenzar en el mundo del blogin con Ghost, puedes llegar a la siguiente pregunta: ¿Como puedo escribir posts en varios idiomas con esta genial herramienta de código abierto que es Ghost?.

Esa fue la pregunta que yo me hice al iniciar el desarrollo de este blog debido a que quería aprovechar las diferentes audiencias de mi area profesional Ingeniería del Software, que existen tanto en el idioma Ingles como en el Español y que estas audiencias se beneficiaran de mi contenido de alto valor que iba a compartir en mis posts.

Lo primero que hice como todo buen auto-didacta fue buscar en Google:

  1. ¿Como agregar i18n a Ghost?
  2. ¿Como escribir un post en varios idiomas en Ghost?

Pero... los resultados no fueron los esperados, encontré este issue de Github [Epic] Ghost and i18n abierto desde el 2014... solo con ver la fecha de apertura ya me daba un mal presentimiento, aún así seguí leyendo comentario a comentario de ese issue y vi como explicaban los miembros de la comunidad las diferentes propuestas para agregar i18n al código de Ghost, más que todo, que herramientas utilizar, que patrones seguir, entre otras cosas.

Finalmente me di cuenta de que ese issue/epic era para el soporte de multiples idiomas pero del admin de Ghost, no para los posts en general.

Como última instancia decidí unirme al Slack de Ghost y preguntar directamente por allí a las personas que tienen más tiempo y experiencia que yo utilizando Ghost. La respuesta fue que todavía no existía soporte oficial para i18n en los posts. Así que decidí tomar acciones en el asunto y crear algo por mi propia cuenta para hacer esto posible.

Crear un plugin desde cero iba a tomarme mucho tiempo, por que primero tenía que analizar a cierto bajo nivel la forma en la que esta construído Ghost para luego en base a eso desarrollar el plugin, pero rápidamente deseché esta opción por el factor tiempo.

La segunda opción que se me ocurrió fue la que implementé y es la siguiente: Levantar dos instancias de Ghost en puertos diferentes y con los proxies de Nginx redireccionar dependiendo del idioma del usuario a una instancia u otra, luego crear un objeto clave/valor donde se almacenen las urls de ambas instancias y agregar un botón con la opción de cambio de idioma.

A continuación explicaré detalladamente el proceso para lograr esta segunda opción de i18n, para este ejemplo he utilizado un contenedor de DigitalOcean específicamente el estándar de 5$ al mes con el sistema operativo Ubuntu 16.04 64 bits y NodeJs v6.9.2.

Instalar Ghost

Lo primero que debemos hacer es instalar Ghost y para esto es recomendado instalarlo en la ruta /var/www/ghost. Vamos a instalar los paquetes zip y wget que usaremos para descargar Ghost, después debemos crear el directorio /var/www/ donde descargaremos la ultima versión de Ghost desde su repositorio de Github:

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

Ahora que hemos obtenido la ultima version de Ghost, tenemos que descomprimirlo. También cambiaremos nuestro directorio a /var/www/ghost/.

sudo unzip -d ghost ghost-latest.zip
cd ghost/

Configurar memoria Swap

Este paso es solo para aquellos que seleccionaron el contenedor de 512 de RAM de DigitalOcean, para los que no siguen el post con el contenedor, pueden continuar con el paso: Instalar dependencias npm de Ghost

Antes de instalar las dependencias npm de Ghost debemos habilitar memoria de tipo Swap, esto para evitar que el sistema operativo detenga el proceso de npm install por falta de memoria.

Swap es un area en el disco duro que ha sido diseñado como un lugar donde el sistema operativo puede temporalmente almacenar información que no puede retener en la memoria RAM. Básicamente, esto te da la habilidad de incrementar la cantidad de información que tu servidor puede mantener en la memoria con algunas limitaciones. El espacio en el disco duro sera usado principalmente cuando el espacio en la memoria RAM sea insuficiente.

Creando el archivo Swap

Crearemos un archivo llamado swapfile en nuestro directorio raíz (/). El archivo debe asignar la cantidad de espacio que queremos para nuestro archivo swap.

Podemos crear un archivo de 2 Gigabytes escribiendo:

sudo fallocate -l 2G /swapfile

Podemos verificar que la cantidad correcta de espacio fue reservada escribiendo:

ls -lh /swapfile

El comando anterior debería imprimir algo como esto:

-rw-r--r-- 1 root root 2.0G Feb 26 17:52 /swapfile

Habilitando el archivo Swap

Actualmente nuestro archivo esta creado, pero nuestro sistema operativo no sabe que este esta destinado para usar como memoria Swap. Necesitamos darle formato a este archivo como Swap y luego habilitarlo. Aunque antes de hacer esto, necesitamos ajustar los permisos en nuestro archivo para que de este modo no sea leíble por alguien mas que no sea root. Permitir a otros usuarios leer o escribir en este archivo puede ser un alto riesgo de seguridad.

Necesitamos bloquear los permisos y para eso escribimos:

sudo chmod 600 /swapfile

Ahora verificamos que el archivo tiene los permisos correctos escribiendo:

ls -lh /swapfile

El comando anterior debería imprimir algo como esto:

-rw------- 1 root root 2.0G Feb 26 17:52 /swapfile

Como pueden ver, únicamente las columnas para el usuario root tienen las propiedades de lectura y escritura habilitadas.

Ahora que nuestro archivo es más seguro, podemos decirle a nuestro sistema operativo que establezca el espacio swap con el comando:

sudo mkswap /swapfile

Nuestro archivo esta listo para ser usado como espacio swap. Podemos habilitarlo con el comando:

sudo swapon /swapfile

Podemos verificar que nuestro procedimiento fue satisfactorio con el comando:

sudo swapon -s

Nuestro espacio Swap ha sido establecido satisfactoriamente en nuestro sistema operativo y comenzará a ser usado cuando sea necesario.

Hacer el archivo Swap permanente

Tenemos nuestro espacio Swap habilitado, pero cuando reiniciamos, el server no lo habilitará automáticamente. Podemos cambiar esto mediante la modificación del archivo fstab.

Editamos el archivo con privilegios root en nuestro editor de texto:

sudo nano /etc/fstab

Al final del archivo, necesitamos agregar una nueva linea que le dirá al sistema operativo que use nuestro archivo Swap automáticamente.

/swapfile   none    swap    sw    0   0

Guardamos y cerramos el archivo una vez hemos terminado.

Instalar dependencias npm de Ghost

Ahora podemos instalar las dependencias de Ghost y los módulos de node (únicamente las dependencias para producción).

sudo npm install --production

Ghost quedará instalado cuando esto se completa. Necesitamos configurar Ghost antes de poder iniciarlo.

Configurar Ghost

El archivo de configuración de Ghost debería estar localizado en /var/www/ghost/config.js. Sin embargo ese archivo no viene con Ghost por defecto, en vez de ese, la instalación incluye config.example.js, entonces copiaremos el ejemplo de configuración en la ubicación correcta.

Nos aseguramos de copiarlo en vez de moverlo en caso de que necesitemos revertir los cambios siempre tendremos el archivo de configuración por defecto.

sudo cp config.example.js config.js

Abrimos el archivo en la terminal:

sudo nano config.js

Tienes que cambiar el valor de la url al de tu dominio (o podrías usar la dirección IP de tu servidor en el caso de que no quieras utilizar un dominio). Este valor debe estar en el formato de una URL. Por ejemplo, http://ejemplo.com/ o http://la_ip_de_tu_servidor/. Si este valor no esta con el formato correcto, Ghost no podrá iniciar.

Cambia también el valor del host en la sección del servidor a 0.0.0.0.

Guarda el archivo y sal del editor de texto nano presionando CTRL + X luego escribes Y y finalmente presionas ENTER.

Instalar Nginx

El siguiente paso es instalar Nginx. El cual básicamente nos va a permitir conectarnos a Ghost a través del puerto 80 y agregar los correspondientes proxies para cada idioma.

Lo instalamos con el siguiente comando:

sudo apt-get install nginx

Lo siguiente que tendremos que hacer es configurar Nginx y por ello movernos al directorio /etc/nginx y eliminar el archivo por defecto en /etc/nginx/sites-enabled

cd /etc/nginx/
sudo rm sites-enabled/default

Crearemos un nuevo archivo en /etc/nginx/sites-available/ llamado ghost y lo abriremos con nano para editarlo:

sudo touch /etc/nginx/sites-available/ghost
sudo nano /etc/nginx/sites-available/ghost

Pega el siguiente código en el archivo y cambia solo el nombre del server con el nombre de tu dominio o la dirección IP de tu servidor en caso de que no quieras agregar un dominio.

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;
  }
}

Guarda el archivo y sal del editor de texto nano presionando CTRL + X luego escribes Y y finalmente presionas ENTER.

Ahora vamos a crear un enlace simbólico a nuestra configuración en sites-enabled:

 sudo ln -s /etc/nginx/sites-available/ghost /etc/nginx/sites-enabled/ghost 

Vamos a reiniciar Nginx:

sudo service nginx restart

Ahora necesitamos iniciar Ghost:

cd /var/www/ghost
npm start --production

Deberías ser capaz de acceder a tu blog en el puerto 80 como http://la_ip_de_tu_servidor/ o http://tu_nombre_de_dominio/.

Crear y configurar otra instancia de Ghost para el idioma adicional

En caso de tener Ghost corriendo, presionamos Ctrl + C o Command + C para detener el proceso. Ahora procederemos a crear otra instancia de Ghost, para esto, estando situados en /var/www/ghost crearemos una carpeta llamada en y moveremos todo el contenido hacia ella:

mkdir en
mv * en

Vamos a crear una carpeta llamada es y copiaremos todo el contenido de la carpeta en a la carpeta es:

mkdir es
cp -r en/* es/

Usaremos la carpeta en para el idioma Ingles y la carpeta es para el idioma Español. Necesitamos modificar algunos archivos dentro de la carpeta es para agregarle el soporte multi idioma, lo primero que modificaremos será el archivo de configuración de Ghost:

cd es/
sudo nano config.js

Tenemos que modificar la propiedad url del objeto de producción, así como también la propiedad server, en url colocaremos http://la_ip_de_tu_servidor/es/blog y dentro de la propiedad server modificaremos el puerto "port" por 2369. Esto lo realizamos con el propósito de que nuestra url de cada instancia de Ghost siga la buena practica SEO de cargar el contenido del blog identificado por el nombre del país en el estándar ISO 3166-1, así como también al momento de iniciar ambas instancias cada una corra en un puerto diferente.

Guardamos el archivo y salimos del editor de texto nano presionando CTRL + X luego escribimos Y y finalmente presionamos ENTER.

El objeto de producción debería quedar así:

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'
  }
}

La modificación anterior al archivo de configuración, debemos realizarla también en la carpeta de ingles en:

Directorio 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'
  }
}

Vamos a agregar la parte HTML correspondiente para mostrar el icono de cambio de idiomas, para esto debemos situarnos en la carpeta casper el cual es el tema por defecto que trae Ghost content/themes/casper/:

cd content/themes/casper/

El siguiente código HTML lo agregaremos justo debajo del botón de Menu que se muestra en la esquina superior derecha de la interfaz y dentro de la etiqueta <nav></nav> en los archivos index.hbs, post.hbs, tag.hbs y author.hbs estas modificaciones las haremos tanto en la carpeta de es para español, como en la carpeta en para ingles:

Version para español - Directorio 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>

Version para ingles - Directorio 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>

La sección de nav de esos archivos debería quedar algo como esto:

Version para español

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

Version para ingles

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

El siguiente paso es agregar los estilos correspondientes para que el icono de cambio de idioma se vea bien tanto en computadores de escritorio como en tabletas y teléfonos mobiles.

Para esto vamos a abrir el archivo screen.css ubicado en es/content/themes/casper/assets/css/screen.css y en/content/themes/casper/assets/css/screen.css justo después de las animaciones agregaremos el siguiente código de estilos:

/* ===============================================================
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;
  }
}

Ya tenemos el botón y los estilos, lo que nos faltaría sería agregar la funcionalidad y es lo que haremos a continuación, necesitaremos modificar los archivos default.hbs que se encuentra en content/themes/casper/default.hbs y index.js ubicado en content/themes/casper/assets/js/index.js.

El primer archivo a modificar sera default.hbs donde agregaremos justo antes de la etiqueta de cierre </body> y después de los scripts:

Version para español - 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>

Version para ingles - 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>

Y justo al inicio del archivo index.js vamos a agregar:

Version para español - 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();
  });
});

Version para ingles - 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();
  });
});

Modificar el archivo de configuración de Nginx

Vamos a modificar el archivo que creamos en el paso Instalar Nginx, para que nuestro servidor apunte a nuestras dos instancias Ghost.

sudo nano /etc/nginx/sites-available/ghost

Eliminamos el contenido que tiene el archivo y pegamos lo siguiente:

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;
  }
}

Ten en cuenta que donde dice nombre_de_mi_pagina_o_ip_de_servidor deberás colocar tanto el nombre de tu página o la ip del servidor en la que estes instalando Ghost.

Guarda el archivo y sal del editor de texto nano presionando CTRL + X luego escribes Y y finalmente presionas ENTER.

Para que Nginx aplique los cambios realizados debemos ejecutar el siguiente comando:

sudo service nginx restart

Encender ambas instancias de Ghost

Vamos a situarnos en ambas instancias para encender los servicios de ghost:

cd /var/www/ghost/en/
npm start --production &
cd /var/www/ghost/es/
npm start --production &

De esa forma ya tenemos nuestras dos instancias de Ghost, para Ingles y Español corriendo en nuestro servidor.

Cambiar la ruta del post por defecto en nuestra instancia de Ghost para Español

Vamos al navegador y colocamos la ruta que le asignamos a nuestra instancia de ghost:

http://nombre_de_mi_pagina_o_ip_de_servidor/es/blog/

Podremos ver que satisfactoriamente nuestra instancia para español esta funcionando...

Ahora vamos a dirigirnos a la sección de administración "admin", que sería:

http://nombre_de_mi_pagina_o_ip_de_servidor/es/blog/admin

Allí veremos la configuración inicial de Ghost para establecer las credenciales de acceso, procedemos a crear nuestra cuenta para luego continuar a la página del panel de control.

Hacemos click en el post que esta por defecto: "Welcome to Ghost", y luego hacemos click en el icono de tuerca que se encuentra en la esquina superior derecha, la cual nos permitira cambiarle la url de nuestro primer post. En el campo "Post URL" colocaremos: bienvenido-a-ghost.

De esta forma ya tenemos configurado nuestro primer post, para que funcione con i18n :)

A medida que creemos más posts debemos ir registrando cada una de sus respectivas URLS en los objetos que contienen las claves de nuestras urls en ambos idiomas. De esta forma la funcionalidad de i18n que agregamos tendrá un mapa de urls disponibles y sabrá cual URL en ingles corresponde a cual URL en español y viceversa.

Ejemplo, supongamos que este post que están leyendo es nuevo
y como URL le hemos asignado adding-internationalization-i18n-to-ghosts-posts para Ingles y agregando-internacionalizacion-i18n-a-los-posts-en-ghost para Español, entonces debemos agregarlo al mapa de las URLS que ya tenemos:

Directorio - 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'
};

Directorio - 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'
};

Ya con eso tendríamos registradas las 2 nuevas URLS en ambos idiomas e instancias.

Eso sería todo por este post, espero que les sea de mucha ayuda y/o utilidad para tener sus post de Ghost en ambos idiomas, cualquier duda, consulta adicional, opinión o cosas en general que piensan que debería mejorar, no duden en comentarlo.

Saludos y un abrazo asíncrono.