Why CodeIgniter 4 Is Ideal for Multi-Language Websites
CodeIgniter 4 stands out for its simplicity, speed, and flexibility. Unlike heavier frameworks, CI4 doesn't impose unnecessary overhead, making it ideal for scalable applications. Its i18n support is handled via the Localization system, which allows you to manage language files, detect user locales, and format content dynamically. This is particularly useful for websites targeting international users, as it handles not just text translations but also numbers, dates, and currencies in a locale-aware manner.
Internationalization (i18n) in CodeIgniter 4 is handled through language files stored inside the app/Language directory. Each language has its own folder, and strings are retrieved using the lang() function.
Key benefits include:
- Easy integration with existing CI4 projects.
- Support for BCP 47 language tags (e.g., en-US, fr-FR).
- Automatic fallback to parent locales or English.
- Built-in tools for locale negotiation via HTTP headers or URL segments.
- Compatibility with the intl PHP extension for advanced formatting.

Table Of Content
1 Prerequisites
- PHP 8.1+ (recommended 8.2 or higher)
- Composer installed
- Basic knowledge of CodeIgniter 4
2 Introduction
This guide walks you step by step through creating a fully functional multi-language website in CodeIgniter 4, covering language files, routing, language switching, SEO considerations, and best practices. By the end, you'll have a production-ready setup that supports English, French, and Spanish — easily extendable to any number of languages.
We'll use CI4's built-in Localization library — no third-party packages required. The approach uses URL-based locale detection (e.g., /en/about, /fr/about) which is the most SEO-friendly method, as search engines can index each language version separately.
3 Create / Install a Codeigniter 4 Project
Use the following command to install new Codeigniter Project.
composer create-project codeigniter4/appstarter multi-lang-ci4
Then, navigate to your project directory:
cd multi-lang-ci4
4 Configuring the Default Locale and Supported Locales
Begin by configuring the core settings in app/Config/App.php. This file controls the default locale and supported languages.
Open app/Config/App.php and update the following:
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class App extends BaseConfig
{
public string $baseURL = 'http://localhost:8080/';
public string $defaultLocale = 'en';
public bool $negotiateLocale = true; // Use browser Accept-Language header
public array $supportedLocales = ['en', 'fr', 'es'];
// Other default settings...
public array $allowedHostnames = [];
public string $indexPage = '';
public string $uriProtocol = 'REQUEST_URI';
public string $defaultTimezone = 'UTC';
// ...
}
Explanation:
- $defaultLocale — Sets the fallback language when no locale is detected.
- $negotiateLocale — Enables CI4 to detect the user's preferred language from the browser's Accept-Language header (requires the intl extension).
- $supportedLocales — Lists all languages your site supports. Stick to valid BCP 47 codes. If a user's requested locale isn't supported, CI4 falls back to the default.
Tip: For production, you can also set app.baseURL in your .env file instead of hardcoding it. Install the intl extension if it's missing: sudo apt install php-intl on Ubuntu, or equivalent for your OS.
5 Creating Locale Filter (Middleware)
Create Filter File: app/Filters/Locale.php
<?php
namespace App\Filters;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;
class Locale implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$session = session();
$uri = $request->getUri();
$segment = $uri->getSegment(1);
$appConfig = config('App');
$supported = $appConfig->supportedLocales;
// URL segment takes highest priority (language switcher clicks)
if (in_array($segment, $supported)) {
$locale = $segment;
$session->set('locale', $locale);
}
// Then check if user has a stored session preference
elseif ($session->has('locale') && in_array($session->get('locale'), $supported)) {
$locale = $session->get('locale');
}
// Fallback to browser negotiation or default
else {
$locale = $request->getLocale();
$session->set('locale', $locale);
}
// Set locale for this request
$request->setLocale($locale);
// Optional: Set PHP locale for date/number formatting
setlocale(LC_ALL, $locale . '.utf8', $locale);
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Nothing to do after
}
}
Register the filter in: app/Config/Filters.php
public array $aliases = [
// ...
'locale' => \App\Filters\Locale::class,
];
Important: Notice the URL segment check comes first, before the session check. This ensures that when a user clicks the language switcher (e.g., navigates to /fr/about), the new language from the URL takes effect immediately and updates the session. If you check the session first, the old language would override the URL and the switcher would appear broken.
6 Creating Language Files
Language files are stored in app/Language/{locale}/, where {locale} is 'en', 'fr', etc. Each file is a PHP array returning key-value pairs for translations.
Create app/Language/en/App.php:
<?php
return [
'welcome' => 'Welcome to Our Multi-Language Site',
'greeting' => 'Hello, {0}! You have {1, number} new messages.',
'about' => 'About Us',
'contact' => 'Contact',
'aboutContent' => 'This is the about page in English.',
'contactContent'=> 'Contact us anytime in English.',
'switchLanguage'=> 'Switch Language',
];
Now, for French — app/Language/fr/App.php:
<?php
return [
'welcome' => 'Bienvenue sur notre site multilingue',
'greeting' => 'Bonjour, {0} ! Vous avez {1, number} nouveaux messages.',
'about' => 'À propos',
'contact' => 'Contact',
'aboutContent' => 'Ceci est la page à propos en français.',
'contactContent'=> 'Contactez-nous à tout moment en français.',
'switchLanguage'=> 'Changer de langue',
];
And for Spanish — app/Language/es/App.php:
<?php
return [
'welcome' => '¡Bienvenido a nuestro sitio multilingüe!',
'greeting' => '¡Hola, {0}! Tienes {1, number} mensajes nuevos.',
'about' => 'Sobre nosotros',
'contact' => 'Contacto',
'aboutContent' => 'Esta es la página de información en español.',
'contactContent' => 'Contáctenos en cualquier momento en español.',
'switchLanguage' => 'Cambiar idioma',
];
Important: Every language file must contain all the same keys. If a key is missing (e.g., switchLanguage missing from the Spanish file), CI4 will return the raw key name like App.switchLanguage instead of the translated text. Always keep your language files in sync across all locales.
Explanation: Keys must be unique and can't start or end with dots. Use nested arrays for organization, e.g., errors.emailMissing. The {0} and {1, number} are placeholders for dynamic values, formatted using ICU syntax. This ensures numbers, dates, etc., respect locale rules (e.g., commas vs. periods for decimals).
7 Create Home Controller
php spark make:controller Home
Open app/Controllers/Home.php and replace its contents:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
class Home extends BaseController
{
protected $helpers = ['url', 'html'];
public function index()
{
$data = [
'title' => lang('App.welcome'),
'message' => lang('App.greeting', ['John Doe', 42]),
'current_locale' => $this->request->getLocale(),
];
return view('home', $data);
}
public function about()
{
$data = [
'title' => lang('App.about'),
'content' => lang('App.aboutContent'),
];
return view('pages/about', $data);
}
public function contact()
{
$data = [
'title' => lang('App.contact'),
'content' => lang('App.contactContent'),
];
return view('pages/contact', $data);
}
}
How it works: The lang() function automatically uses the current request locale to fetch the correct translation. The second argument in lang('App.greeting', ['John Doe', 42]) passes dynamic values to the ICU placeholders {0} and {1, number} defined in your language files.
8 Create Layout & View Files
Create app/Views/layouts/main.php:
<!DOCTYPE html>
<html lang="<?= service('request')->getLocale() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= esc($title ?? 'Home') ?> - Multi-Language CI4</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?php
$currentLocale = service('request')->getLocale();
// Get path without the locale prefix for language switcher
$path = uri_string();
$supported = config('App')->supportedLocales;
$segments = explode('/', trim($path, '/'));
if (!empty($segments[0]) && in_array($segments[0], $supported)) {
array_shift($segments);
}
$pathWithoutLocale = '/' . implode('/', $segments);
?>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/<?= $currentLocale ?>/">CI4 i18n</a>
<div class="navbar-nav ms-auto">
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown">
<?= lang('App.switchLanguage') ?> (<?= strtoupper($currentLocale) ?>)
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/en<?= $pathWithoutLocale ?>">English</a></li>
<li><a class="dropdown-item" href="/fr<?= $pathWithoutLocale ?>">Français</a></li>
<li><a class="dropdown-item" href="/es<?= $pathWithoutLocale ?>">Español</a></li>
</ul>
</div>
</div>
</div>
</nav>
<main class="container mt-5">
<?= $this->renderSection('content') ?>
</main>
<footer class="container mt-5 py-3 text-center text-muted">
<p>© <?= date('Y') ?> Multi-Language CI4 Demo</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Create app/Views/home.php:
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<h1><?= esc($title) ?></h1>
<p class="lead"><?= esc($message) ?></p>
<p>Current locale: <strong><?= esc($current_locale) ?></strong></p>
<div class="mt-4">
<a href="/<?= esc($current_locale) ?>/about" class="btn btn-outline-primary"><?= lang('App.about') ?></a>
<a href="/<?= esc($current_locale) ?>/contact" class="btn btn-outline-secondary"><?= lang('App.contact') ?></a>
</div>
<?= $this->endSection() ?>
Create app/Views/pages/about.php:
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<h1><?= esc($title) ?></h1>
<p><?= esc($content) ?></p>
<a href="/<?= service('request')->getLocale() ?>/" class="btn btn-primary">← Back to Home</a>
<?= $this->endSection() ?>
Create app/Views/pages/contact.php:
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<h1><?= esc($title) ?></h1>
<p><?= esc($content) ?></p>
<a href="/<?= service('request')->getLocale() ?>/" class="btn btn-primary">← Back to Home</a>
<?= $this->endSection() ?>
Note: In the layout file, we strip the current locale prefix from the URL path before building the language switcher links. Without this, switching from /fr/about to English would produce /en/fr/about (double locale) instead of the correct /en/about.
Also notice we use service('request') to access the request object in views, and lang('App.about') for button labels so they translate correctly.
9 Define a Route
To make your site truly multi-language, you need to detect and switch locales dynamically using URL-based locale detection.
Edit app/Config/Routes.php to include a {locale} placeholder:
<?php
use CodeIgniter\Router\RouteCollection;
/**
* @var RouteCollection $routes
*/
// Enable strict locale checking (404 if unsupported locale)
$routes->useSupportedLocalesOnly(true);
// Routes with {locale} prefix
$routes->group('{locale}', ['filter' => 'locale'], static function ($routes) {
$routes->get('/', 'Home::index', ['as' => 'home']);
$routes->get('about', 'Home::about', ['as' => 'about']);
$routes->get('contact', 'Home::contact', ['as' => 'contact']);
});
// Fallback: redirect root to default locale
$routes->get('/', static function () {
return redirect()->to('/' . service('request')->getLocale() . '/');
});
Explanation:
- The {locale} segment (e.g., /en/, /fr/about) automatically sets the locale via IncomingRequest.
- useSupportedLocalesOnly(true) returns a 404 for unsupported locales (e.g., /de/about if German isn't in your supported list).
- The filter => locale applies our Locale middleware to all routes in the group.
- The root / fallback redirects to the user's detected locale (via browser negotiation or default).
This URL-based approach is the most SEO-friendly method, as each language version has a unique, indexable URL that search engines can crawl independently.
10 Project Structure Overview
app/
├── Config/
│ ├── App.php
│ └── Routes.php
├── Controllers/
│ └── Home.php
├── Views/
│ ├── layouts/
│ │ └── main.php
│ ├── home.php
│ └── pages/
│ ├── about.php
│ └── contact.php
├── Language/
│ ├── en/
│ │ └── App.php
│ ├── fr/
│ │ └── App.php
│ └── es/
│ └── App.php
11 Run and Test
php spark serve
Now test the following URLs:
- http://localhost:8080/ → automatically redirects to /en/ (or your browser's preferred language)
- http://localhost:8080/en/ → English home page
- http://localhost:8080/fr/about → French about page
- http://localhost:8080/es/contact → Spanish contact page
- http://localhost:8080/de/ → should return 404 (unsupported locale)
Try switching languages using the dropdown in the navbar. The language should persist across page navigation via the session, and the URL should always reflect the active locale.
Best Practices for CI4 Multi-Language Websites
Here are some proven best practices to keep your multi-language CodeIgniter 4 project maintainable and scalable:
- Keep language files in sync: Every locale folder should have the same file names and keys. Missing keys will display raw key strings to users.
- Use nested keys for large projects: Organize translations by feature — e.g., Auth.loginButton, Auth.logoutButton, Dashboard.welcomeBack.
- Add hreflang tags for SEO: In your layout, add <link rel="alternate" hreflang="en" href="/en/..."> for each supported language. This helps Google serve the correct language version in search results.
- Handle RTL languages: If you plan to support Arabic or Hebrew, add a dir="rtl" attribute to the <html> tag conditionally based on the active locale.
- Cache translations in production: CI4 caches language files automatically, but ensure your writable/cache directory is properly configured.
- Secure your app: Multi-language sites have more URL surface area. Make sure to protect against SQL injection and validate all user inputs.
- Test with real users: Automated tests catch code bugs, but native speakers catch translation issues. Have someone review each language version.
For authentication in your multi-language site, check out our CI4 Authentication Tutorial with Shield. If you're building an API backend alongside your multi-language frontend, see our CI4 REST API CRUD guide.
12 Conclusion
CodeIgniter 4 provides a clean, fast, and scalable i18n system for building multi-language websites without relying on third-party packages. By following this guide, you now have a production-ready setup that is SEO-friendly, maintainable, and easily extendable to additional languages.
Whether you're building a corporate website, SaaS platform, or content-heavy portal, this approach ensures your application speaks your users' language — literally.
What to Build Next
Now that your CI4 app supports multiple languages, here are some related tutorials to level up your project:
- Add authentication with CI4 Shield — Login & Registration System
- Build a REST API with CI4 — CRUD Example with Postman
- Implement Role-Based Access Control in CI4
- Secure Your CI4 Application from SQL Injection
- Boost CI4 Performance and Page Speed
If you found this tutorial helpful, share it with your fellow developers and bookmark it for future reference!
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
Add the locale code (e.g., "de" for German) to the $supportedLocales array in app/Config/App.php, then create a matching folder under app/Language/de/ with all your translation files containing the same keys as your other language files.
The most common cause is the Locale filter checking the session before the URL segment. Make sure your filter checks the URL segment first, then falls back to the session. Also verify the filter is registered and applied to your route group.
Add link tags in your layout head section like: <link rel="alternate" hreflang="en" href="/en/page"> for each supported locale. This tells search engines which language version to serve to users in different regions.
i18n stands for internationalization, which in CI4 is handled by the Localization library for multi-language support.
Use URL prefixes like /en/ or /fr/, or browser negotiation, and store preferences in sessions.
Yes, via ICU placeholders in lang() with the intl extension.
Yes, create matching files in app/Language to override.
