Por que internacionalizar la API
Tu API tiene un cliente en Madrid, otro en Berlin y otro en Tokio. Si todos reciben:
{ "error": "Este campo es obligatorio." }
Berlin y Tokio tienen que traducir cada mensaje en el frontend, multiplicado por cada validacion. Hay un patron mejor: el servidor responde en el idioma que el cliente pide.
GET /api/productos/ HTTP/1.1
Accept-Language: en-US, en;q=0.9, es;q=0.5
Y el servidor responde:
{ "error": "This field is required." }
Aplica especialmente a APIs B2B publicas y apps moviles con audiencia internacional. Para una API privada interna mono-idioma, no merece la pena.
Configuracion base
# settings.py
USE_I18N = True
USE_L10N = True
LANGUAGE_CODE = "es-es" # idioma por defecto si no hay Accept-Language
LANGUAGES = [
("es", "Espanol"),
("en", "English"),
("fr", "Francais"),
("de", "Deutsch"),
]
LOCALE_PATHS = [BASE_DIR / "locale"]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware", # importante: tras Session
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
]
LocaleMiddleware examina (en orden):
- La sesion (
session.LANGUAGE_SESSION_KEY). - La cookie
django_language. - El header
Accept-Language. LANGUAGE_CODEpor defecto.
Para una API REST sin cookies, lo unico relevante es
Accept-Language.
Marcar strings traducibles en serializers
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
class ProductoSerializer(serializers.ModelSerializer):
nombre = serializers.CharField(
label = _("Nombre"),
help_text = _("Nombre comercial del producto"),
)
precio = serializers.DecimalField(
max_digits = 10,
decimal_places = 2,
label = _("Precio"),
)
def validate_precio(self, value):
if value <= 0:
raise serializers.ValidationError(_("El precio debe ser positivo."))
return value
class Meta:
model = Producto
fields = ["id", "nombre", "precio"]
gettext_lazy (alias _) marca el string para traduccion sin evaluarlo aun. Se traduce en runtime cuando el serializer construye la respuesta, segun el idioma activo.
Generar y compilar archivos .po
django-admin makemessages -l en -l fr -l de
Genera locale/<idioma>/LC_MESSAGES/django.po:
#: tienda/serializers.py:5
msgid "Nombre"
msgstr "Name"
#: tienda/serializers.py:14
msgid "El precio debe ser positivo."
msgstr "Price must be positive."
Tras editar, compilar:
django-admin compilemessages
Recarga el server. Ahora con Accept-Language: en los labels y errores salen en ingles.
Modelos con campos traducidos
Si los datos tambien son multi-idioma (titulo y descripcion del producto), instalar:
pip install django-modeltranslation
# tienda/translation.py
from modeltranslation.translator import register, TranslationOptions
from tienda.models import Producto
@register(Producto)
class ProductoTR(TranslationOptions):
fields = ("nombre", "descripcion")
# settings.py
INSTALLED_APPS = ["modeltranslation", ...] # antes de tus apps
MODELTRANSLATION_LANGUAGES = ("es", "en", "fr", "de")
MODELTRANSLATION_DEFAULT_LANGUAGE = "es"
Tras makemigrations + migrate, la tabla productos tendra nombre_es, nombre_en, nombre_fr, nombre_de. El acceso producto.nombre devuelve el del idioma activo automaticamente.
class ProductoSerializer(serializers.ModelSerializer):
class Meta:
model = Producto
fields = ["id", "nombre", "descripcion", "precio"]
# 'nombre' resuelve al campo correcto segun Accept-Language
Cuidado con la busqueda.
Producto.objects.filter(nombre__icontains="phone")solo busca en el campo del idioma activo. Para busqueda multi-idioma, usaQ(nombre_es__icontains=...) | Q(nombre_en__icontains=...).
Forzar idioma desde el frontend
Si el cliente prefiere control explicito, soporta tambien un query param:
# core/middleware.py
from django.utils import translation
class QueryLanguageMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
lang = request.GET.get("lang")
if lang in dict(settings.LANGUAGES).keys():
translation.activate(lang)
request.LANGUAGE_CODE = lang
return self.get_response(request)
Ahora GET /api/productos/?lang=fr fuerza frances independientemente del header.
Tests por idioma
from rest_framework.test import APITestCase
from django.test import override_settings
class ProductoI18nTests(APITestCase):
def test_error_validacion_en_ingles(self):
r = self.client.post(
"/api/productos/",
data = {"nombre": "test", "precio": "-10"},
HTTP_ACCEPT_LANGUAGE = "en",
)
self.assertEqual(r.status_code, 400)
self.assertIn("Price must be positive", str(r.data))
def test_error_validacion_en_espanol(self):
r = self.client.post(
"/api/productos/",
data = {"nombre": "test", "precio": "-10"},
HTTP_ACCEPT_LANGUAGE = "es",
)
self.assertIn("debe ser positivo", str(r.data))
Formato de fechas y numeros
DRF usa el formato de Django segun el locale. 2026-04-18 puede serializarse como 18/04/2026 en es-ES y 04/18/2026 en en-US si configuras los serializer fields con format=None (auto-locale) en vez de format="%Y-%m-%d".
Recomendacion: para APIs, devuelve siempre ISO 8601 (
2026-04-18T10:00:00Z). El formato localizado es para UI; la API permite que cada cliente formatee como quiera.
Documentar idiomas soportados en OpenAPI
drf-spectacular permite anadir info i18n al schema:
SPECTACULAR_SETTINGS = {
"TITLE": "Mi API",
"VERSION": "1.0.0",
"DESCRIPTION": "API multi-idioma. Usa Accept-Language para elegir.",
"SERVERS": [{"url": "https://api.miapp.com/", "description": "Prod"}],
"COMPONENT_SPLIT_REQUEST": True,
"EXTERNAL_DOCS": {
"url": "https://docs.miapp.com/i18n",
"description": "Idiomas soportados: es, en, fr, de",
},
}
Diagrama del flujo
flowchart LR
C[Cliente: Accept-Language: en] --> MW[LocaleMiddleware]
MW --> AC[translation.activate en]
AC --> V[Vista DRF]
V --> S[Serializer con _(...)]
S --> RES[Response<br>label/error en ingles]
RES --> C
Buenas practicas
- Marca todo lo visible con
_: labels, help_text, mensajes de error, choices. - No marques constantes internas (codigos, slugs, ids). Solo lo que va al usuario.
- Manten los .po actualizados en CI: un job que ejecute
makemessagesy falle si hay strings nuevos sin traducir. - Documenta los locales soportados en tu OpenAPI / pagina de developers.
- Usa ISO 8601 para fechas/horas en respuestas JSON.
- Cuidado con plurales: usa
ngettext_lazypara "1 producto" / "5 productos".
Errores comunes
- Olvidar
LocaleMiddleware: nunca cambia el idioma. - Cambiar el
LANGUAGE_CODEglobal en runtime en vez detranslation.activate. El primero requiere reiniciar el server. - Usar
gettexten lugar degettext_lazyen codigo de modulo (ej:class Meta:).gettextse evalua al import y devuelve siempre el idioma por defecto. - No tener
USE_I18N=True: las cadenas no se traducen aunque las marques con_.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Django
Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Configurar LocaleMiddleware y LANGUAGES soportados. Marcar mensajes con gettext_lazy en serializers, models, views. Generar y compilar archivos .po con makemessages/compilemessages. Anadir campos traducidos a modelos con django-modeltranslation. Servir errores de validacion en el idioma del cliente. Testear con APITestCase en distintos idiomas. Documentar locales soportados en OpenAPI.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje