IncludeBeer

Creating a multilingual website with CodeIgniter 4 (part 2)

Published on 27 June 2021
Article's image
Image credit: Brett Zeck

In the first part we created a multilingual website with translation files. But if you have a blog, you don't want to write your articles in translation files and have to edit code to display them. The solution is to save the articles in the database and display only the articles in the selected language. This is very easy. Here is how to do it...

The language files

First, add the following translations to the application's language files. They will be used in the article list.

app/Language/de/Blog.php

'noArticles' => "Keine Artikel",
'readMore' => "Mehr lesen",

app/Language/en/Blog.php

'noArticles' => "No articles",
'readMore' => "Read more",

app/Language/es/Blog.php

'noArticles' => "No hay artículos",
'readMore' => "Leer más",

app/Language/fr/Blog.php

'noArticles' => "Aucun articles",
'readMore' => "Lire la suite",

app/Language/ja/Blog.php

'noArticles' => "記事なし",
'readMore' => "続きを読む",

The routes

Add the following route to display an article from its slug. We define the placeholder slug in the same way as we did in the tutorial for a basic application.

app/Config/Routes.php

$routes->addPlaceholder('slug', '[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*');
$routes->get('{locale}/(:slug)', 'HomeController::article/$1');

Database

For the database, instead of creating a SQL script, we will use the Migration class. The command spark make:migration allows us to create an empty class as a starting point for our migration. You just have to pass it the name of the class to create as a parameter.

cd /Applications/MAMP/htdocs/ci4.test
/Applications/MAMP/bin/php/php7.4.2/bin/php spark make:migration CreateBlogTables

It creates a file in the directory app/Database/Migrations/ with the current date and time and the name of the class: 2021-06-24-080000_Createblogtables.php

<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class Createblogtables extends Migration
{
        public function up()
        {
                //
        }

        public function down()
        {
                //
        }
}

Even though we asked to create a CreateBlogTables class, CodeIgniter created the Createblogtables class for us. The file name or the class name is not important, so I rename them to make it more readable. I put underscores in the file name and capitalize the class name. The only thing to be careful with is the date and time. Because if we have several migration files, it is in this order that they will be applied. So here is our migration class to create an article table. It is the lang field that will allow us to get the articles in the user's language.

app/Database/Migrations/2021-06-24-080000_create_blog_tables.php
<?php namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateBlogTables extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id' => ['type' => 'integer', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
            'slug' => ['type' => 'varchar', 'constraint' => 255],
            'lang' => ['type' => 'varchar', 'constraint' => 5, 'null' => true],
            'title' => ['type' => 'varchar', 'constraint' => 150],
            'text' => ['type' => 'text', 'null' => true],
            'created_at' => ['type' => 'datetime'],
            'updated_at' => ['type' => 'datetime'],
        ]);

        $this->forge->addPrimaryKey('id');
        $this->forge->createTable('article');
    }

    //--------------------------------------------------------------------

    public function down()
    {
        $this->forge->dropTable('article');
    }
}

Then, to create our dummy articles, we'll use a Seeder class. We will simply loop from 1 to 5 for each language and insert lorem ipsum text. Only the title and the first words of the articles are translated so that we can see that we get the articles in the right language.

app/Database/Seeds/BlogSeeder.php

<?php namespace App\Database\Seeds;

use CodeIgniter\Database\Seeder;

class BlogSeeder extends Seeder
{
    public function run()
    {
        $builder = $this->db->table('article');
        $nb_article = 5;

        // DE
        for ($article_no = 1; $article_no <= $nb_article; $article_no++)
        {
            $row = [
                'slug'          => "article-{$article_no}",
                'lang'          => 'de',
                'title'         => "Artikel in Deutsch {$article_no}",
                'text'          => <<<EOT
Artikel in Deutsch. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi eros, ultrices vitae arcu fringilla, sollicitudin tempus lacus. Curabitur suscipit ex eu nisi volutpat, quis pharetra neque dictum. Sed commodo est blandit facilisis vehicula. Ut justo mauris, ultricies eu massa ac, dignissim porttitor ligula. Quisque nec magna ante. Nullam accumsan nunc sed accumsan ornare. Donec orci mi, lobortis sed purus quis, egestas viverra justo. Cras pulvinar, mauris ut efficitur venenatis, urna justo suscipit tortor, nec sodales lacus quam sed dui. Sed auctor, augue vel scelerisque molestie, ligula odio consequat ipsum, vel facilisis sapien velit non risus. Cras at molestie mauris. Nam a euismod purus. Cras vitae commodo orci, vel tempus tortor. Ut sodales, est ac volutpat congue, dui tortor luctus elit, sit amet accumsan lectus diam a tortor. Aenean convallis, elit vel scelerisque accumsan, enim nisi volutpat lacus, in interdum nulla elit nec nunc. Nullam eleifend elit cursus mauris elementum porta.

Nulla et dui at metus pretium accumsan sed et ligula. Integer sodales cursus elit in commodo. Suspendisse ultrices diam elit, sit amet fermentum dui vulputate sit amet. Aenean a eros nec arcu tempus consectetur id cursus quam. Cras quis arcu pellentesque, fringilla nisl vitae, accumsan elit. Praesent nec semper velit, a posuere diam. Quisque in arcu eros. Vivamus venenatis purus risus, ut vehicula turpis ullamcorper nec. Morbi purus leo, lobortis sollicitudin mi sit amet, blandit aliquet orci. Donec tincidunt consequat felis vitae bibendum. Ut porttitor ante eget dui volutpat venenatis. Mauris eget euismod neque, dapibus blandit eros. Cras condimentum metus ac mi tempor, in commodo orci vestibulum. Aliquam a ultricies leo. Integer quis auctor ligula. Nulla sed congue justo.
EOT
                ,
                'created_at'    => date('Y-m-d H:i:s'),
                'updated_at'    => date('Y-m-d H:i:s'),
            ];
            $builder->insert($row);
        }

        // EN
        for ($article_no = 1; $article_no <= $nb_article; $article_no++)
        {
            $row = [
                'slug'          => "article-{$article_no}",
                'lang'          => 'en',
                'title'         => "Article in English {$article_no}",
                'text'          => <<<EOT
Article in English. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi eros, ultrices vitae arcu fringilla, sollicitudin tempus lacus. Curabitur suscipit ex eu nisi volutpat, quis pharetra neque dictum. Sed commodo est blandit facilisis vehicula. Ut justo mauris, ultricies eu massa ac, dignissim porttitor ligula. Quisque nec magna ante. Nullam accumsan nunc sed accumsan ornare. Donec orci mi, lobortis sed purus quis, egestas viverra justo. Cras pulvinar, mauris ut efficitur venenatis, urna justo suscipit tortor, nec sodales lacus quam sed dui. Sed auctor, augue vel scelerisque molestie, ligula odio consequat ipsum, vel facilisis sapien velit non risus. Cras at molestie mauris. Nam a euismod purus. Cras vitae commodo orci, vel tempus tortor. Ut sodales, est ac volutpat congue, dui tortor luctus elit, sit amet accumsan lectus diam a tortor. Aenean convallis, elit vel scelerisque accumsan, enim nisi volutpat lacus, in interdum nulla elit nec nunc. Nullam eleifend elit cursus mauris elementum porta.

Nulla et dui at metus pretium accumsan sed et ligula. Integer sodales cursus elit in commodo. Suspendisse ultrices diam elit, sit amet fermentum dui vulputate sit amet. Aenean a eros nec arcu tempus consectetur id cursus quam. Cras quis arcu pellentesque, fringilla nisl vitae, accumsan elit. Praesent nec semper velit, a posuere diam. Quisque in arcu eros. Vivamus venenatis purus risus, ut vehicula turpis ullamcorper nec. Morbi purus leo, lobortis sollicitudin mi sit amet, blandit aliquet orci. Donec tincidunt consequat felis vitae bibendum. Ut porttitor ante eget dui volutpat venenatis. Mauris eget euismod neque, dapibus blandit eros. Cras condimentum metus ac mi tempor, in commodo orci vestibulum. Aliquam a ultricies leo. Integer quis auctor ligula. Nulla sed congue justo.
EOT
                ,
                'created_at'    => date('Y-m-d H:i:s'),
                'updated_at'    => date('Y-m-d H:i:s'),
            ];
            $builder->insert($row);
        }

        // ES
        for ($article_no = 1; $article_no <= $nb_article; $article_no++)
        {
            $row = [
                'slug'          => "article-{$article_no}",
                'lang'          => 'es',
                'title'         => "Artículo en español {$article_no}",
                'text'          => <<<EOT
Artículo en español. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi eros, ultrices vitae arcu fringilla, sollicitudin tempus lacus. Curabitur suscipit ex eu nisi volutpat, quis pharetra neque dictum. Sed commodo est blandit facilisis vehicula. Ut justo mauris, ultricies eu massa ac, dignissim porttitor ligula. Quisque nec magna ante. Nullam accumsan nunc sed accumsan ornare. Donec orci mi, lobortis sed purus quis, egestas viverra justo. Cras pulvinar, mauris ut efficitur venenatis, urna justo suscipit tortor, nec sodales lacus quam sed dui. Sed auctor, augue vel scelerisque molestie, ligula odio consequat ipsum, vel facilisis sapien velit non risus. Cras at molestie mauris. Nam a euismod purus. Cras vitae commodo orci, vel tempus tortor. Ut sodales, est ac volutpat congue, dui tortor luctus elit, sit amet accumsan lectus diam a tortor. Aenean convallis, elit vel scelerisque accumsan, enim nisi volutpat lacus, in interdum nulla elit nec nunc. Nullam eleifend elit cursus mauris elementum porta.

Nulla et dui at metus pretium accumsan sed et ligula. Integer sodales cursus elit in commodo. Suspendisse ultrices diam elit, sit amet fermentum dui vulputate sit amet. Aenean a eros nec arcu tempus consectetur id cursus quam. Cras quis arcu pellentesque, fringilla nisl vitae, accumsan elit. Praesent nec semper velit, a posuere diam. Quisque in arcu eros. Vivamus venenatis purus risus, ut vehicula turpis ullamcorper nec. Morbi purus leo, lobortis sollicitudin mi sit amet, blandit aliquet orci. Donec tincidunt consequat felis vitae bibendum. Ut porttitor ante eget dui volutpat venenatis. Mauris eget euismod neque, dapibus blandit eros. Cras condimentum metus ac mi tempor, in commodo orci vestibulum. Aliquam a ultricies leo. Integer quis auctor ligula. Nulla sed congue justo.
EOT
                ,
                'created_at'    => date('Y-m-d H:i:s'),
                'updated_at'    => date('Y-m-d H:i:s'),
            ];
            $builder->insert($row);
        }

        // FR
        for ($article_no = 1; $article_no <= $nb_article; $article_no++)
        {
            $row = [
                'slug'          => "article-{$article_no}",
                'lang'          => 'fr',
                'title'         => "Article en français {$article_no}",
                'text'          => <<<EOT
Article en français. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi eros, ultrices vitae arcu fringilla, sollicitudin tempus lacus. Curabitur suscipit ex eu nisi volutpat, quis pharetra neque dictum. Sed commodo est blandit facilisis vehicula. Ut justo mauris, ultricies eu massa ac, dignissim porttitor ligula. Quisque nec magna ante. Nullam accumsan nunc sed accumsan ornare. Donec orci mi, lobortis sed purus quis, egestas viverra justo. Cras pulvinar, mauris ut efficitur venenatis, urna justo suscipit tortor, nec sodales lacus quam sed dui. Sed auctor, augue vel scelerisque molestie, ligula odio consequat ipsum, vel facilisis sapien velit non risus. Cras at molestie mauris. Nam a euismod purus. Cras vitae commodo orci, vel tempus tortor. Ut sodales, est ac volutpat congue, dui tortor luctus elit, sit amet accumsan lectus diam a tortor. Aenean convallis, elit vel scelerisque accumsan, enim nisi volutpat lacus, in interdum nulla elit nec nunc. Nullam eleifend elit cursus mauris elementum porta.

Nulla et dui at metus pretium accumsan sed et ligula. Integer sodales cursus elit in commodo. Suspendisse ultrices diam elit, sit amet fermentum dui vulputate sit amet. Aenean a eros nec arcu tempus consectetur id cursus quam. Cras quis arcu pellentesque, fringilla nisl vitae, accumsan elit. Praesent nec semper velit, a posuere diam. Quisque in arcu eros. Vivamus venenatis purus risus, ut vehicula turpis ullamcorper nec. Morbi purus leo, lobortis sollicitudin mi sit amet, blandit aliquet orci. Donec tincidunt consequat felis vitae bibendum. Ut porttitor ante eget dui volutpat venenatis. Mauris eget euismod neque, dapibus blandit eros. Cras condimentum metus ac mi tempor, in commodo orci vestibulum. Aliquam a ultricies leo. Integer quis auctor ligula. Nulla sed congue justo.
EOT
                ,
                'created_at'    => date('Y-m-d H:i:s'),
                'updated_at'    => date('Y-m-d H:i:s'),
            ];
            $builder->insert($row);
        }

        // JA
        for ($article_no = 1; $article_no <= $nb_article; $article_no++)
        {
            $row = [
                'slug'          => "article-{$article_no}",
                'lang'          => 'ja',
                'title'         => "日本語での記事 {$article_no}",
                'text'          => <<<EOT
日本語での記事. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi eros, ultrices vitae arcu fringilla, sollicitudin tempus lacus. Curabitur suscipit ex eu nisi volutpat, quis pharetra neque dictum. Sed commodo est blandit facilisis vehicula. Ut justo mauris, ultricies eu massa ac, dignissim porttitor ligula. Quisque nec magna ante. Nullam accumsan nunc sed accumsan ornare. Donec orci mi, lobortis sed purus quis, egestas viverra justo. Cras pulvinar, mauris ut efficitur venenatis, urna justo suscipit tortor, nec sodales lacus quam sed dui. Sed auctor, augue vel scelerisque molestie, ligula odio consequat ipsum, vel facilisis sapien velit non risus. Cras at molestie mauris. Nam a euismod purus. Cras vitae commodo orci, vel tempus tortor. Ut sodales, est ac volutpat congue, dui tortor luctus elit, sit amet accumsan lectus diam a tortor. Aenean convallis, elit vel scelerisque accumsan, enim nisi volutpat lacus, in interdum nulla elit nec nunc. Nullam eleifend elit cursus mauris elementum porta.

Nulla et dui at metus pretium accumsan sed et ligula. Integer sodales cursus elit in commodo. Suspendisse ultrices diam elit, sit amet fermentum dui vulputate sit amet. Aenean a eros nec arcu tempus consectetur id cursus quam. Cras quis arcu pellentesque, fringilla nisl vitae, accumsan elit. Praesent nec semper velit, a posuere diam. Quisque in arcu eros. Vivamus venenatis purus risus, ut vehicula turpis ullamcorper nec. Morbi purus leo, lobortis sollicitudin mi sit amet, blandit aliquet orci. Donec tincidunt consequat felis vitae bibendum. Ut porttitor ante eget dui volutpat venenatis. Mauris eget euismod neque, dapibus blandit eros. Cras condimentum metus ac mi tempor, in commodo orci vestibulum. Aliquam a ultricies leo. Integer quis auctor ligula. Nulla sed congue justo.
EOT
                ,
                'created_at'    => date('Y-m-d H:i:s'),
                'updated_at'    => date('Y-m-d H:i:s'),
            ];
            $builder->insert($row);
        }
    }
}

Model and entity

The model and the entity for the articles are the simplest thing. If you have gone through my article series How to build a basic web application with CodeIgniter 4, you already know how this works.

app/Models/ArticleModel.php

<?php namespace App\Models;

use CodeIgniter\Model;
use App\Entities\Article;

class ArticleModel extends Model
{
    protected $table = 'article';
    protected $primaryKey = 'id';
    protected $returnType = Article::Class;
    protected $allowedFields = ['slug', 'lang', 'title', 'text', 'created_at', 'updated_at'];
    protected $useTimestamps = true;
}

app/Entities/Article.php

<?php namespace App\Entities;

use CodeIgniter\Entity;

Class Article extends Entity
{
    protected $dates = ['created_at', 'updated_at'];
}

The controller

In the controller, replace the index() function and add the article() function. The index() function gets the list of articles in the requested language. We saw in part 1 of this article that the language is determined by the first segment of the URL. For example, for the URL http://ci4.test:8888/es, the first segment is es. This segment is automatically extracted thanks to the {locale} term defined in our routes. In the BaseController we have saved the language in the variable $this->viewData['locale']. It is this variable that we use in the query to get the articles in the right language.

The article() function gets an article from its slug. It also specifies the requested language in case the slug is the same in several languages. For example, if we have an article about iPhones, the slug could be iphone in all languages, which would give the following URLs: http://ci4.test:8888/en/iphone, http://ci4.test:8888/es/iphone, etc. Even in our dummy data, the slugs are the same in all languages (article-1, article-2, article-3, etc).

app/Controllers/HomeController.php

<?php

namespace App\Controllers;

class HomeController extends BaseController
{
    public function index()
    {
        $articleModel = model('articleModel');

        $this->viewData['articles'] = $articleModel
            ->where('lang', $this->viewData['locale'])
            ->orderBy('title', 'asc')
            ->find();

        return view('index', $this->viewData);
    }

    public function article (string $slug)
    {
        $articleModel = model('articleModel');

        $this->viewData['article'] = $articleModel
            ->where('lang', $this->viewData['locale'])
            ->where('slug', $slug)
            ->first();

        if ( ! $this->viewData['article'])
        {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        return view('article', $this->viewData);
    }
}

The views

Replace the index.php view with the code below. There is nothing very complicated. We loop over the list of articles to display the title with a link to the article. We build the links with the anchor() function:

<?= anchor($article->lang . '/' . $article->slug, lang('Blog.readMore')) ?> &rarr;

This will give the following result:

<a href="http://ci4.test:8888/en/article-1">Read more</a> &rarr;

The code &rarr; will display an arrow pointing to the right.

app/Views/index.php

<?php
/**
 * @var \CodeIgniter\View\View $this
 * @var array $articles
 * @var \App\Entities\Article $article
 */
?>
<?= $this->extend('template') ?>
<?= $this->section('main_content') ?>

        <section class="px-3 py-5">
            <div class="container">
                <div class="mb-5">
                    <h3><?= lang('Blog.aboutTitle') ?></h3>
                    <p><?= lang('Blog.aboutText') ?></p>
                </div>

<?php if (count($articles) != 0) : ?>
<?php   foreach ($articles as $article) : ?>
                <div class="mb-5">
                    <h3><?= $article->title ?></h3>
                    <?= anchor($article->lang . '/' . $article->slug, lang('Blog.readMore')) ?> &rarr;
                </div>
<?php   endforeach; ?>

            </div>
        </section>
<?php else : ?>
        <div class="alert alert-info text-center"><?= lang('Blog.noArticles') ?></div>
<?php endif; ?>

<?= $this->endSection() ?>

Create the new view article.php. In this view we display the text of the article. We use the function nl2br() to convert the line breaks into <br>. To make the visual more interesting, I added a placeholder image thanks to https://placekitten.com/.

app/Views/article.php

<?php
/**
 * @var \CodeIgniter\View\View $this
 * @var \App\Entities\Article $article
 */
?>
<?= $this->extend('template') ?>
<?= $this->section('main_content') ?>

    <article class="px-3 py-5">
        <div class="container">
            <header>
                <h2 class="title mb-2"><?= $article->title ?></h2>
            </header>
            <div class="m-5">
                <img src="https://placekitten.com/200/287" alt="placekitten.com" class="float-left mr-4 mb-4">
                <p><?= nl2br($article->text) ?></p>
            </div>
        </div>
    </article>

<?= $this->endSection() ?>

Run the database migration and the Seeder

Now that everything is in place, we can run the migration script to create the MySQL table with the command spark migrate -all. We can then insert our dummy data to test our application by running the Seeder. Run the following commands in a terminal:

cd /Applications/MAMP/htdocs/ci4.test
/Applications/MAMP/bin/php/php7.4.2/bin/php spark migrate -all
/Applications/MAMP/bin/php/php7.4.2/bin/php spark db:seed BlogSeeder

We can also refresh the database with the migrate:refresh parameter. This will call the down() and up() function of our migration class. This means that the MySQL table will be destroyed and then recreated. Then we just call the Seeder to insert the test data. This is useful if we make adjustments to the table structure or if we edit the test data and want to return to the initial state.

cd /Applications/MAMP/htdocs/ci4.test
/Applications/MAMP/bin/php/php7.4.2/bin/php spark migrate:refresh
/Applications/MAMP/bin/php/php7.4.2/bin/php spark db:seed BlogSeeder

Conclusion

You now have a website in five languages, with the possibility to add dynamic content in several languages in the database! And thanks to CodeIgniter 4 it was super easy!

Beer If you enjoy my tutorials, you can buy me the equivalent of a coffee, a beer or a lunch!

Paypal