Skip to main content

Simple translatable Django fields

Project description

django-garnett

Django Garnett is a field level translation library that allows you to store strings in multiple languages for fields in Django - with minimal changes to your models and without having to rewrite your code.

Want a demo? https://django-garnett.herokuapp.com/

Made with by Aristotle Metadata

In summary it allows you to do this:

models.py You can do this!
By changing your models from this...
class Greeting(models.model):
    text = CharField(max_length=150)
    target = models.CharField()
    def __str__(self):
        return f"{self.greeting}, {self.target}"

to this...

# Import garnett
from garnett.fields import Translated

class Greeting(models.model):
    # Convert greeting to a translatable field
    text = Translated(CharField(max_length=150))
    target = models.CharField()
    def __str__(self):
        return f"{self.greeting} {self.target}"
greeting = Greeting(text="Hello", target="World")

with set_field_language("en"):
    greeting.text = "Hello"
with set_field_language("fr"):
    greeting.text = "Bonjour"

greeting.save()
greeting.refresh_from_db()

with set_field_language("en"):
    print(greeting.text)
    print(greeting)
# >>> "Hello"
# >>> "Hello World"

with set_field_language("fr"):
    print(greeting.text)
    print(greeting)
# >>> "Bonjour"
# >>> "Bonjour World!"

with set_field_language("en"):
    print(greeting.text)
    print(greeting)
# >>> "Hello"
# >>> "Hello World"
    Greeting.objects.filter(title="Hello").exists()
# >>> True
    Greeting.objects.filter(title="Bonjour").exists()
# >>> False
    Greeting.objects.filter(title__fr="Bonjour").exists()
# >>> True!!

# Assuming that GARNETT_DEFAULT_TRANSLATABLE_LANGUAGE="en"
# Or a middleware has set the language context
print(greeting.text)
# >>> Hello
print(greeting)
# >>> Hello World!

Tested on:

  • Django 3.1+
  • Postgres, SQLite, MariaDB
  • Python 3.7+

Pros:

  • Battletested in production - Aristotle Metadata built, support and uses this library for 2 separate products, served to government and enterprise clients!
  • Fetching all translations for a model requires a single query
  • Translations are stored in a single database field with the model
  • All translations act like a regular fieldModel.field_name = "some string" and print(Model.field_name) work as you would expect
  • Includes a configurable middleware that can set the current language context based on users cookies, query string or HTTP headers
  • Works nicely with Django Rest Framework - translatable fields can be set as strings or as json dictionaries
  • Works nicely with Django F() and Q() objects within the ORM - and when it doesn't we have a language aware LangF() replacement you can use.

Cons:

  • You need to alter the models, so you can't make third-party libraries translatable.

Why write a new Django field translator?

A few reasons:

  • Most existing django field translation libraries are static, and add separate database columns per translation.
  • We needed a library that could be added in without requiring a rewrite of a very large code base.

Note: Field language is different to the django display language. Django can be set up to translate your pages based on the users browser and serve them with a user interface in their preferred language.

Garnett does not use the browser language by design - a user with a French browser may want the user interface in French, but want to see content in English or French based on their needs.

How to install

  1. Add django-garnett to your dependencies. eg. pip install django-garnett

  2. Convert your chosen field using the Translated function

    • For example: title = fields.Translated(models.CharField(*args))
  3. Add GARNETT_TRANSLATABLE_LANGUAGES (a callable or list of language codes) to your django settings.

    Note: At the moment there is no way to allow "a user to enter any language".

  4. Add GARNETT_DEFAULT_TRANSLATABLE_LANGUAGE (a callable or single language code) to your settings.

  5. Re-run django makemigrations & django migrate for any apps you've updated.

  6. Thats mostly it.

You can also add a few optional value adds:

  1. (Optional) Add a garnett middleware to take care of field language handling:

    • You want to capture the garnett language in a context variable available in views use: garnett.middleware.TranslationContextMiddleware

    • You want to capture the garnett language in a context variable available in views, and want to raise a 404 if the user requests an invalid language use: garnett.middleware.TranslationContextNotFoundMiddleware

    • (Future addition) You want to capture the garnett language in a context variable available in views, and want to redirect to the default language if the user requests an invalid language use: garnett.middleware.TranslationContextRedirectDefaultMiddleware

    • If you want to cache the current language in session storage use garnett.middleware.TranslationCacheMiddleware after one of the above middleware (this is useful with the session selector mentioned below)

  2. (Optional) Add the garnett app to your INSTALLED_APPS to use garnett's template_tags. If this is installed before django.contrib.admin it also include a language switcher in the Django Admin Site.

  3. (Optional) Add a template processor:

    • Install garnett.context_processors.languages this will add garnett_languages (a list of available Languages) and garnett_current_language (the currently selected language).
  4. (Optional) Add a custom translation fallback:

    By default, if a language isn't available for a field, Garnett will show a mesage like:

    No translation of this field available in English

    You can override this either by creating a custom fallback method:

    Translated(CharField(max_length=150), fallback=my_fallback_method))
    

    Where my_fallback_method takes a dictionary of language codes and corresponding strings, and returns the necessary text.

    Additionally, you can customise how django outputs text in templates by creating a new TranslationStr class, and overriding the __html__ method.

Language vs language

Django Garnett uses the python langcodes library to determine more information about the languages being used - including the full name and local name of the language being used. This is stored as a Language object.

Django Settings options:

  • GARNETT_DEFAULT_TRANSLATABLE_LANGUAGE
    • Stores the default language to be used for reading and writing fields if no language is set in a context manager or by a request.
    • By default it is 'en-AU' the language code for 'Strayan, the native tongue of inhabitants of 'Straya (or more commonly known as Australia).
    • Can also be callable that returns default language code
    • default: 'en-AU'
  • GARNETT_TRANSLATABLE_LANGUAGES:
    • Stores a list of language codes that users can use to save against TranslatableFields.
    • Can also be callable that returns list of language codes
    • default [GARNETT_DEFAULT_TRANSLATABLE_LANGUAGE]
  • GARNETT_REQUEST_LANGUAGE_SELECTORS:
    • A list of string modules that determines the order of options used to determine the language selected by the user. The first selector found is used for the language for the request, if none are found the DEFAULT_LANGUAGE is used. These can any of the following in any order:
      • garnett.selector.query: Checks the GARNETT_QUERY_PARAMETER_NAME for a language to display
      • garnett.selector.cookie: Checks for a cookie called GARNETT_LANGUAGE_CODE for a language to display. Note: you cannot change this cookie name.
      • garnett.selector.session: Checks for a session key GARNETT_LANGUAGE_CODE for a language to display. Note: you cannot change this key name.
      • garnett.selector.header: Checks for a HTTP Header called X-Garnett-Language-Code for a language to display. Note: you cannot change this Header name.
      • garnett.selector.browser: Uses Django's get_language function to get the users browser/UI language as determined by Django.
    • For example, if you only want to check headers and cookies in that order, set this to ['garnett.selectors.header', 'garnett.selectors.cookie'].
    • default: ['garnett.selectors.header', 'garnett.selectors.query', 'garnett.selectors.cookie']
  • GARNETT_QUERY_PARAMETER_NAME:
    • The query parameter used to determine the language requested by a user during a HTTP request.
    • default: glang

Advanced Settings (you probably don't need to adjust these)

  • GARNETT_TRANSLATABLE_FIELDS_PROPERTY_NAME:
    • Garnett adds a property to all models that returns a list of all TranslatableFields. By default, this is 'translatable_fields', but you can customise it here if you want.
    • default: translatable_fields
  • GARNETT_TRANSLATIONS_PROPERTY_NAME:
    • Garnett adds a property to all models that returns a dictionary of all translations of all TranslatableFields. By default, this is 'translations', but you can customise it here if you want.
    • default: translations

Using Garnett with Django-Rest-Framework

As TranslationFields are based on JSONField, by default Django-Rest-Framework renders these as a JSONField, which may not be ideal.

You can get around this by using the TranslatableSerializerMixin as the first mixin, which adds the necessary hooks to your serializer. This will mean class changes, but you won't need to update or override every field.

For example:

from rest_framework import serializers
from library_app import models
from garnett.drf import TranslatableSerializerMixin


class BookSerializer(TranslatableSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = models.Book
        fields = "__all__"

This will allow you to set the value for a translatable as either a string for the active langauge, or by setting a dictionary that has all languages to be saved (note: this will override the existing language set). For example:

To override just the active language:

curl -X PATCH ... -d "{  \"title\": \"Hello\"}"

To specifically override a single language (for example, Klingon):

curl -X PATCH ...  -H  "X-Garnett-Language-Code: tlh" -d "{  \"title\": \"Hello\"}"

To override all languages:

curl -X PATCH ... -d "{  \"title\": {\"en\": \"Hello\", \"fr\": \"Bonjour\"}}"

Why call it Garnett?

  • Libraries need a good name.
  • Searching for "Famous Translators" will tell you about Constance Garnett.
  • Searching for "Django Garnett" showed there was no python library with this name.
  • It did however talk about Garnet Clark (also spelled Garnett), a jazz pianist who played with Django Reinhart - the namesake of the Django Web Framework.
  • Voila - a nice name

Warnings

  • contains == icontains - On SQLite only, when doing a contains query it does a case insensitive search. I don't know why - https://www.youtube.com/watch?v=PgGNWRtceag
  • Due to how django sets admin form fields you will not get the admin specific widgets like AdminTextAreaWidget on translated fields in the django admin site by default. They can however be specified explicitly on the corresponding admin model form.

need to run tests like this for now: PYTHONPATH=../ ./manage.py shell

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-garnett-0.3.0.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

django_garnett-0.3.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

Details for the file django-garnett-0.3.0.tar.gz.

File metadata

  • Download URL: django-garnett-0.3.0.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.8.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.12

File hashes

Hashes for django-garnett-0.3.0.tar.gz
Algorithm Hash digest
SHA256 1b91c30b2eebbe6e9cb1625987c145f636962649ddd27c76cbe47124ee9b71e7
MD5 672c13186f75d68c1add4995a90b164c
BLAKE2b-256 21543d403b7cb60de3e53a5dc5f26663fd714bf6987523fd483a5137493da86a

See more details on using hashes here.

Provenance

File details

Details for the file django_garnett-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: django_garnett-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 19.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.8.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.12

File hashes

Hashes for django_garnett-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d430340ea09f99ba8918b07f60e5632730281b21cb615924bd195a15aa4e7880
MD5 419430fe8e317b2543fbccab0f636d5f
BLAKE2b-256 74c52a01b4a78422cc846e5f14a3d6d4458f9a68800b0ead1086fd29007c7981

See more details on using hashes here.

Provenance

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page