Mostrar 1.234,56 en España, $1,234.56 en Estados Unidos o 1.234,56 en Alemania con el mismo código PHP es exactamente para lo que existe la extensión intl. Va mucho más allá de cambiar el separador decimal: gestiona plurales, fechas localizadas, comparación de cadenas y ordenación según las reglas de cada idioma.
Instalar y verificar intl
php -m | grep intl
En Ubuntu/Debian: sudo apt install php-intl && sudo phpenmod intl && sudo systemctl restart php8.x-fpm. En Windows ya suele venir incluida en el paquete XAMPP o Laragon.
NumberFormatter: números y monedas
<?php
// Número con formato local
$fmt = new NumberFormatter('es_ES', NumberFormatter::DECIMAL);
echo $fmt->format(1234567.89); // 1.234.567,89
$fmt = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
echo $fmt->format(1234567.89); // 1,234,567.89
// Moneda
$fmt = new NumberFormatter('es_ES', NumberFormatter::CURRENCY);
echo $fmt->formatCurrency(1234.5, 'EUR'); // 1.234,50
$fmt = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
echo $fmt->formatCurrency(1234.5, 'USD'); // $1,234.50
// Porcentaje
$fmt = new NumberFormatter('es_ES', NumberFormatter::PERCENT);
echo $fmt->format(0.1234); // 12 %
// Redondeo
$fmt = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
echo $fmt->format(3.14159); // 3,14
Parsear un número localizado
Igual de útil que formatear: convertir lo que el usuario escribe en su idioma a un float de PHP:
<?php
$fmt = new NumberFormatter('es_ES', NumberFormatter::DECIMAL);
$valor = $fmt->parse('1.234,56');
echo $valor; // 1234.56 (float)
$fmt = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
$moneda = '';
$valor = $fmt->parseCurrency('$1,234.50', $moneda);
echo "$valor $moneda"; // 1234.5 USD
IntlDateFormatter: fechas localizadas
<?php
$fecha = new DateTime('2026-09-18');
// Formato largo en español
$fmt = new IntlDateFormatter(
'es_ES',
IntlDateFormatter::LONG, // fecha
IntlDateFormatter::NONE, // hora
'Europe/Madrid'
);
echo $fmt->format($fecha); // 18 de septiembre de 2026
// Formato corto en inglés americano
$fmt = new IntlDateFormatter(
'en_US',
IntlDateFormatter::SHORT,
IntlDateFormatter::SHORT,
'America/New_York'
);
echo $fmt->format($fecha); // 9/18/26, 2:00 AM
// Patrón personalizado (ICU)
$fmt = new IntlDateFormatter('es_ES', 0, 0, 'Europe/Madrid', null, "EEEE, d 'de' MMMM 'de' yyyy");
echo $fmt->format($fecha); // viernes, 18 de septiembre de 2026
MessageFormatter: plurales y mensajes compuestos
El problema del plural varía por idioma: en inglés "1 item / 2 items"; en ruso hay tres formas distintas. MessageFormatter usa la sintaxis ICU para resolverlo sin if/else:
<?php
// Plural simple en español
$patron = '{count, plural, one {# artículo encontrado} other {# artículos encontrados}}';
echo MessageFormatter::formatMessage('es_ES', $patron, ['count' => 1]);
// 1 artículo encontrado
echo MessageFormatter::formatMessage('es_ES', $patron, ['count' => 5]);
// 5 artículos encontrados
// Mensaje con nombre y moneda
$patron = '{nombre} tiene {saldo, number, currency} en su cuenta.';
$fmt = new MessageFormatter('es_ES', $patron);
echo $fmt->format(['nombre' => 'Ana', 'saldo' => 1250.75]);
// Ana tiene 1.250,75 en su cuenta.
// Select por género
$patron = '{genero, select, male {Bienvenido} female {Bienvenida} other {Bienvenido/a}}, {nombre}.';
echo MessageFormatter::formatMessage('es_ES', $patron, ['genero' => 'female', 'nombre' => 'María']);
// Bienvenida, María.
Collator: comparar y ordenar cadenas
El operador < de PHP ordena por valor de byte Unicode, lo que da resultados incorrectos para muchos idiomas. Collator aplica las reglas de ordenación del locale:
<?php
$palabras = ['zebra', 'ñoño', 'árbol', 'acacia', 'Árbol'];
// Ordenación nativa PHP incorrecta
sort($palabras);
print_r($palabras); // Árbol, acacia, árbol, ñoño, zebra (Á antes de a)
// Ordenación con Collator español
$col = new Collator('es_ES');
$col->sort($palabras);
print_r($palabras); // acacia, árbol, Árbol, ñoño, zebra
// Comparación directa
$resultado = $col->compare('árbol', 'acacia');
// > 0 ? árbol va después de acacia
// Ordenar array de objetos por campo
usort($registros, fn($a, $b) => $col->compare($a->nombre, $b->nombre));
Locale: detectar y negociar el idioma del usuario
<?php
// Negociar locale a partir de Accept-Language del navegador
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'es-ES';
$locale = Locale::acceptFromHttp($header);
echo $locale; // es_ES, en_US, etc.
// Obtener el idioma primario
echo Locale::getPrimaryLanguage($locale); // es
// Información del locale
echo Locale::getDisplayName('es_ES', 'es'); // español (España)
echo Locale::getDisplayName('zh_CN', 'es'); // chino simplificado (China)
Combinando todo: clase de presentación localizada
<?php
class Presentacion {
public function __construct(private string $locale) {}
public function moneda(float $importe, string $divisa = 'EUR'): string {
$fmt = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
return $fmt->formatCurrency($importe, $divisa);
}
public function fecha(DateTimeInterface $dt, string $zona = 'Europe/Madrid'): string {
$fmt = new IntlDateFormatter($this->locale, IntlDateFormatter::LONG, IntlDateFormatter::NONE, $zona);
return $fmt->format($dt);
}
public function plural(string $patron, array $args): string {
return MessageFormatter::formatMessage($this->locale, $patron, $args);
}
}
$p = new Presentacion('es_ES');
echo $p->moneda(1999.99); // 1.999,99
echo $p->fecha(new DateTime('now')); // 18 de septiembre de 2026
echo $p->plural('{n, plural, one {# pedido} other {# pedidos}}', ['n' => 3]);
// 3 pedidos
Errores frecuentes
- Confundir locale y codificación: el locale define el formato visual; la codificación del string sigue siendo UTF-8. Son cosas distintas.
- ICU no actualizada: si el sistema tiene una versión antigua de libicu, algunos locales o patrones ICU no funcionan.
echo INTL_ICU_VERSION;para verificar. - formatCurrency sin divisa: pasar
nullcomo divisa devuelve una cadena vacía o falla. Pasa siempre el código ISO 4217 ('EUR', 'USD', 'GBP'). - parse() devuelve false: si el formato no coincide exactamente con el locale. Comprueba el valor de retorno antes de usarlo.
La extensión intl es la pieza que falta cuando se pretende que una aplicación PHP funcione bien en varios países al mismo tiempo. Con cuatro clases cubre el 95% de los casos de internacionalización sin instalar paquetes externos.
