Contents
- 1 PHP Localization basics
- 1.1 Why bother with localization in PHP?
- 1.2 The foundation: setlocale and categories
- 1.3 Dates, numbers, money: Hands-on examples
- 1.4 Strings and translations: Arrays first
- 1.5 Level up: Gettext for pro translations
- 1.6 The Locale class: Modern polish
- 1.7 Frameworks and beyond
- 1.8 Common pitfalls and fixes
- 1.9 Wrapping your head around it
PHP Localization basics
Hey, fellow developers. Picture this: it's 2 AM, your coffee's gone cold, and you're staring at a form validation error that reads perfectly in English but comes out mangled in Spanish—commas where decimals should be, dates flipped upside down. We've all been there. That frustration? It's the spark that turns a local app into something global. PHP localization isn't just a checkbox for fancy resumes on platforms like Find PHP—it's the quiet hero making your code feel at home anywhere.
Today, we're diving into the basics. No fluff, just the hands-on stuff that gets your app speaking multiple languages without breaking a sweat. Whether you're hiring out your skills or building that next gig-worthy project, mastering this opens doors. Let's break it down, step by step, with code you can steal and tweak right now.
Why bother with localization in PHP?
You know the drill. A user in Brazil sees "$1,234.56" and thinks it's a thousand bucks. Chaos. Localization—often called l10n (10 letters between L and N)—adapts your app to language, region, currency, dates. PHP's got built-in tools for this, no frameworks required at first.
Think about your last project. Hardcoded strings? Fine for prototypes. But scale to users across borders, and you're debugging culture clashes. Get it right, and your code shines on job boards—PHP developers fluent in i18n stand out.
Key wins:
- User trust: Dates like "11/04/2026" vs. "04/11/2026" don't confuse anyone.
- SEO boost: Localized content ranks better in non-English searches.
- Efficiency: Tools like gettext handle translations scalably.
I've lost nights to this. Once, a client's e-commerce site tanked sales in Europe because prices showed wrong. Fixed it with setlocale. Lesson learned.
The foundation: setlocale and categories
PHP's setlocale() is your entry point. It sets the app's "locale"—a string like "en_US" or "ru_RU"—globally affecting formatting.
Basic syntax:
setlocale(int $category, string $locale);
$category picks what to tweak:
- LC_ALL: Everything. Start here.
- LC_NUMERIC: Decimals, thousands separators. English: 1,234.56. French: 1 234,56.
- LC_TIME: Dates via strftime(). US: %m/%d/%Y. UK: %d/%m/%Y.
- LC_MONETARY: Currency magic.
- LC_MESSAGES: For translations.
Quick test—drop this in a file:
<?php
echo 1.234; // 1.234
setlocale(LC_NUMERIC, 'ru_RU');
echo 1.234; // 1,234
setlocale(LC_NUMERIC, 0); // Reset, returns current
See the switch? Numbers dance to the locale's tune. Pass 0 as locale to query what's active.
Pro tip: Check if it worked. setlocale returns false on failure—missing locales on the server kill you silently.
if (!setlocale(LC_ALL, $locale)) {
// Fallback to en_US
setlocale(LC_ALL, 'en_US');
}
Pair with putenv("LC_ALL=$locale") for environment consistency. Servers can be picky.
Dates, numbers, money: Hands-on examples
Let's make it real. Formatting a price for different regions.
<?php
$price = 1234.56;
setlocale(LC_MONETARY, 'en_US');
echo money_format('%n', $price); // $1,234.56
setlocale(LC_MONETARY, 'fr_FR');
echo money_format('%n', $price); // 1 234,56 €
setlocale(LC_MONETARY, 'ja_JP');
echo money_format('%n', $price); // ¥1234
localeconv() spills all details:
setlocale(LC_MONETARY, 'en_US');
print_r(localeconv());
// Array with decimal_point, thousands_sep, int_curr_symbol, etc.
For dates:
setlocale(LC_TIME, 'en_US');
echo strftime('%x'); // 04/11/26
setlocale(LC_TIME, 'de_DE');
echo strftime('%x'); // 11.04.26
strftime() placeholders:
- %Y: Full year.
- %m: Month (01-12).
- %d: Day (01-31).
- %X: Time.
Users expect this. Get it wrong, they bounce.
Strings and translations: Arrays first
Simplest start: PHP arrays. No extensions needed. Create lang files.
Folder setup:
localized/
en.php
es.php
ru.php
en.php:
<?php
return [
'welcome' => 'Welcome!',
'about' => 'About us',
'price' => 'Price: %s'
];
es.php:
<?php
return [
'welcome' => '¡Bienvenido!',
'about' => 'Sobre nosotros',
'price' => 'Precio: %s'
];
Helper functions in functions.php:
<?php
session_start();
function setLanguage($lang) {
$_SESSION['lang'] = $lang;
}
function getLanguage() {
return $_SESSION['lang'] ?? 'en';
}
function __($key) {
$lang = getLanguage();
$strings = include "localized/{$lang}.php";
return $strings[$key] ?? $key;
}
index.php:
<?php
require_once 'functions.php';
if (isset($_GET['lang'])) {
setLanguage($_GET['lang']);
}
?>
<!DOCTYPE html>
<html lang="<?php echo getLanguage(); ?>">
<head>
<title><?php echo __('welcome'); ?></title>
</head>
<body>
<h1><?php echo __('welcome'); ?></h1>
<p><?php printf(__('price'), '$99'); ?></p>
<nav>
<a href="?lang=en">English</a> |
<a href="?lang=es">Español</a>
</nav>
</body>
</html>
Click links—bam, switches. Session remembers. Fallback to key if missing.
Have you tried this on a side project? It's addictive. Scales to dozens of langs easily.
Level up: Gettext for pro translations
Arrays rock for small apps. But for teams? Gettext. Industry standard. Extracts strings, pros translate PO files, compiles to MO binaries.
Setup:
locale/
en/LC_MESSAGES/messages.mo
es/LC_MESSAGES/messages.mo
Init in a bootstrap file:
<?php
$lang = $_GET['locale'] ?? 'en';
putenv("LC_ALL=$lang");
setlocale(LC_ALL, $lang);
$domain = 'messages';
bindtextdomain($domain, 'locale');
textdomain($domain);
Use:
echo gettext('welcome'); // Or shorthand: _('welcome')
Extract strings with xgettext:
xgettext --from-code=UTF-8 -k_ -o messages.pot *.php
Tools like Poedit or Soluling turn POT into PO/MO. Deploy MO files only—fast lookups.
Switch domains with dgettext($domain, 'string'). Multiple domains? Bind 'em all.
Fallbacks: Default locale as msgid speeds things up. User prefers French? Switch seamlessly.
Real talk—I rebuilt a client's dashboard with gettext. Translation agency handled PO files. Zero code changes post-launch.
The Locale class: Modern polish
PHP's Locale class (intl extension) handles display names, defaults.
use Locale;
echo Locale::getDefault(); // en_US
Locale::setDefault('fr_FR');
echo Locale::getDisplayName('fr_FR', 'en'); // French (France)
Great for dropdowns: "Español (España)". Pairs with setlocale for full power.
Frameworks and beyond
Most frameworks wrap this nicely.
- Laravel: Lang files, __() helper.
- Symfony: Translator component on gettext.
- CakePHP: Gettext native.
Check your stack—don't reinvent.
Common pitfalls and fixes
Ever hit "locale not supported"? Server missing it. Fallback chain:
$locales = ['ru_RU.UTF-8', 'ru_RU', 'ru', 'en_US'];
foreach ($locales as $locale) {
if (setlocale(LC_ALL, $locale)) break;
}
UTF-8 everywhere. Non-ASCII? LC_CTYPE.
Numbers persist wrong? Categories overlap—LC_ALL resets.
Sessions beat GET for lang persistence. Browser headers?
$locale = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
Test on prod-like servers. Docker images often lack locales—apt-get install.
Wrapping your head around it
Remember that 2 AM bug? Localization fixed my sanity. Start simple—arrays for prototypes, gettext for real apps. Experiment. Your next PHP job might hinge on "Can you handle i18n?"
Build something global this weekend. Feel the app breathe across borders. That's the quiet power of code done right.