DEV Community

Cover image for Leveraging Headers for Dynamic Localization in Django
Steve Yonkeu
Steve Yonkeu

Posted on

Leveraging Headers for Dynamic Localization in Django

Ahhhh it's been long I passed here! I was cooking some else as usual. Please calm down, by the end of this talk I will have three good news for you!

Let's speak about cultural diversity, localization, internalization and translations. In a few steps let's discuss about how (it is done) and how (best way to do it). On your marks... get set... ready.... GOOOOOOOOOOOO!!!!!!

Yes to API translations

I will be good to give a brief run down of the steps we are to go through.

Workshop RoadMap

Project Setup

mkdir TransTab

cd TransTab

python -m venv venv

source venv/bin/activate

pip install django boto3 djangorestframework polib python-dotenv pip-chill

pip-chill > requirements.txt --no-chill

django-admin startproject transtab .

python manage.py startapp translator

mkdir utils/

touch translator/serializer.py translator/urls.py utils.py middleware.py renderer.py .env
Enter fullscreen mode Exit fullscreen mode

At this point in time, we should have a folder structure and output as this when running python manage.py runserver:

    .
    ├── manage.py
+   ├── middleware.py
+   ├── requirements.txt
    ├── translator
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── models.py
+   │   ├── serializer.py
    │   ├── tests.py
+   │   ├── urls.py
    │   └── views.py
    ├── transtab
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── utils
+       ├── renderer.py
+       └── translate.py

    4 directories, 20 files
Enter fullscreen mode Exit fullscreen mode

Django Application Running

Creating Translation Middleware

Inside the middleware.py file, add the following content:

from django.utils import translation
from django.conf import settings

class TRSLocaleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        language = request.headers.get('Accept-Language', settings.LANGUAGE_CODE)
        translation.activate(language)
        request.LANGUAGE_CODE = translation.get_language()
        response = self.get_response(request)
        translation.deactivate()
        return response
Enter fullscreen mode Exit fullscreen mode

JSON Response Render

Inspired by a Postman Blog post, Let's customize the renderer.py file with the content below.

# utils/renderer.py
from rest_framework.renderers import JSONRenderer
from rest_framework.views import exception_handler
from django.utils import timezone
import uuid

class NewJSONRenderer(JSONRenderer):
    def render(self, data, accepted_media_type=None, renderer_context=None):
        status_code = renderer_context['response'].status_code

        if 200 <= status_code < 300:
            response = {
                "status": "success",
                "statusCode": status_code,
                "data": data
            }
        else:
            response = {
                "status": "error",
                "statusCode": status_code,
                "error": {
                    "code": data.get('code', 'UNKNOWN_ERROR'),
                    "message": data.get('detail', str(data)),
                    "details": data.get('details', None),
                    "timestamp": timezone.now().isoformat(),
                    "path": renderer_context['request'].path,
                    "suggestion": data.get('suggestion', None)
                }
            }

        response["requestId"] = str(uuid.uuid4())
        response["documentation_url"] = "https://api.example.com/docs/errors"

        return super().render(response, accepted_media_type, renderer_context)

def trans_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if response is not None:
        response.data['status'] = 'error'
        response.data['statusCode'] = response.status_code

    return response
Enter fullscreen mode Exit fullscreen mode

AWS Translate Function

Enter the utils/translate.py file and change the content to:

import os
import boto3
import polib
from django.conf import settings

def translate_text(text, source_language, target_language):
    translate = boto3.client(
        "translate",
        region_name=settings.AWS_REGION,
        aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
        aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY
    )

    result = translate.translate_text(
        Text=text,
        SourceLanguageCode=source_language,
        TargetLanguageCode=target_language
    )
    return result.get("TranslatedText")


def translate_po_files():
    dirs = [
        d for d in os.listdir("locale") if os.path.isdir(os.path.join("locale", d))
    ]
    for dir in dirs:
        po = polib.pofile(f"locale/{dir}/LC_MESSAGES/django.po")
        target_language = po.metadata['Language']
        for entry in po:
            if not entry.translated():
                translated_text = translate_text(entry.msgid, 'auto', target_language)
                entry.msgstr = translated_text
        po.save()

Enter fullscreen mode Exit fullscreen mode

Settings configuration

Let's do a few changes here and there in the settings.py and .env

# settings.py
import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = True if os.getenv('DEBUG') == 'True' else False

#...

AWS_REGION = os.getenv('AWS_REGION')
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY ')

MIDDLEWARE = [
    # ...
    'middleware.TRSLocaleMiddleware',
    # ...
]

INSTALLED_APPS = [
     # ...
     'translator',
]

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'utils.renderer.NewJSONRenderer',
    ),
    'EXCEPTION_HANDLER': 'utils.renderer.trans_exception_handler'
}
Enter fullscreen mode Exit fullscreen mode

Then we add our environment variables:

DEBUG=True
SECRET_KEY='This should look like a secret key'
AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
Enter fullscreen mode Exit fullscreen mode

Hey!!! Is there someone here? Hope we are together, don't sleep (yet)!!!

Thinking about automation

Automating the translation with a command

Now let's automate the translation by creating a custom manage.py command in django.

Creating the command file

mkdir -p translator/management/command

touch translator/management/commands/translate_po.py
Enter fullscreen mode Exit fullscreen mode

After this command running, we have this structure:

    .
    ├── db.sqlite3
    ├── manage.py
    ├── middleware.py
    ├── requirements.txt
    ├── translator
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
+   │   ├── management
+   │   │   └── commands
+   │   │       └── translate_po.py
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── models.py
    │   ├── serializer.py
    │   ├── tests.py
    │   ├── urls.py
    │   └── views.py
    ├── transtab
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── utils
        ├── renderer.py
        └── translate.py

    6 directories, 21 files
Enter fullscreen mode Exit fullscreen mode

Let's drop the command logic now in the code base

from django.core.management.base import BaseCommand
from utils.translate import translate_po_files

class Command(BaseCommand):
    help = 'Translates all .po files using AWS Translate'

    def handle(self, *args, **options):
        translate_po_files()languages.
        self.stdout.write(self.style.SUCCESS('Successfully translated .po files.'))
Enter fullscreen mode Exit fullscreen mode

Hello 👋, are you here? If you are missing anything refer to this repo

Some commands to run:

# make messages for a language
django-admin makemessages -l <lang_code> --ignore=venv/*

# compiling messages, converting from `.po` to `.mo`
django-admin compilemessages --ignore=venv/*

# our new command we made
python manage.py translate_po
Enter fullscreen mode Exit fullscreen mode

That's all folks!!!

Wait a minute.... Who remember about my first line talking about the 3 good news? Ohh you forgot about...
thinking meme pawpaw
So, a few days ago, I decided to be more social media focused and each content I make it directed towards learning with fun. I have a todo for you:

Ciao ciao!!!

Top comments (0)