Internacionalizacion (i18n) en APIs DRF

Avanzado
Django
Django
Actualizado: 19/04/2026

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_CODE por 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, usa Q(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 makemessages y 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_lazy para "1 producto" / "5 productos".

Errores comunes

  • Olvidar LocaleMiddleware: nunca cambia el idioma.
  • Cambiar el LANGUAGE_CODE global en runtime en vez de translation.activate. El primero requiere reiniciar el server.
  • Usar gettext en lugar de gettext_lazy en codigo de modulo (ej: class Meta:). gettext se 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 - Autor del tutorial

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