© Ben Lopatin 2020
B. LopatinDjango Standalone Appshttps://doi.org/10.1007/978-1-4842-5632-9_13

13. Internationalization

Ben Lopatin1 
(1)
New York, NY, USA
 

Internationalization and localization allow applications to be used by people in different languages and using different written contexts (e.g., date formats). Conceptually simple, it’s a powerful way to make software available to more people.

As a native English speaker in a predominantly English-speaking country, I think it’s fair to say that most native English speakers in English-speaking countries give little thought to how their software will be used by people who speak other languages or by those in other countries. However, writing software for only one language “market” works against your benefit since the cost to you to make the software available in other languages is fairly low, and the consequence is a larger user base, both end users and potential contributors.

There are several steps to making your standalone Django app useful to speakers of other languages, which are simultaneously sequential and in order of priority.

Why translation

As a simple example, let’s assume your app includes a form class that performs some basic validation. In our case, it checks to see if the value provided in the coupon field matches a currently active Coupon. If it doesn’t, then the data does not validate, and an error string is returned with the form to be displayed to the user.
class CouponForm(forms.Form):
    coupon = forms.CharField(required=False)
        def clean_coupon(self):
                data = self.cleaned_data.get('coupon', '')
                if data and not Coupon.objects.active().filter(code=data).exists():
                        raise forms.ValidationError(
                                "Invalid coupon code"
                        )
                return data
Now for every user of your app, the validation message displayed will always be “You have entered an invalid coupon code” regardless of what language(s) they have their site configured for. If you wanted to provide this in Spanish, instead, you’d need to check for the field or specific message in your Django project and then return a custom message.
class CouponView(FormView):
        def form_invalid(self, form):
                context = super().get_context_data(form=form)
                spanish_errors = {}
                if (
                        form.errors.get("coupon", [""])[0] ==
                        "Invalid coupon code"
                ):
                        spanish_errors["coupon"] = "Cupón inválido"
                context["spanish_errors"] = spanish_errors
                return self.render_to_response(context)

This is obviously a contrived example which you can probably see some ways to simplify already. But it would be nice if such custom changes where wholly unnecessary.

And with a few minor tweaks, they are unnecessary.

Translatable strings and how translation works

The solution can be implemented wholly in the form class with one import and “wrapping” the string in a function call to gettext:
from django.utils.translation import gettext as _
class CouponForm(forms.Form):
    coupon = forms.CharField(required=False)
        def clean_coupon(self):
                data = self.cleaned_data.get('coupon', '')
                if data and not Coupon.objects.active().filter(code=data).exists():
                        raise forms.ValidationError(
                                _("Invalid coupon code")
                        )
                return data

I’ll call it “wrapping” the string because using the common _ alias that’s what it looks like, but make no mistake about it, this is a function call. When executed, the string returned will be sourced by using the external program gettext based on the locale set in the calling context, which will either be the default locale or the locale chosen by the end user in their session.

In this manner internationalization is nothing more than a simple dictionary lookup. Unlike an English-Spanish dictionary, however, there are no subtle options for a chosen word or phrase; rather this lookup behaves more like a Python dictionary where every string is an exact key that returns another string.

A common objection to internationalization is that the original developer doesn’t know what the potential languages are or know them well enough to provide translations, so there’s little point in making the effort. Fortunately, there are no such requirements to making standalone apps translatable, or even translated!

Prioritizing translation steps

The first step in enabling translation is making your strings translatable. At its simplest, this means “wrapping” your strings in a call to one of the gettext functions as previously described. There are a couple of gettext functions in the django.utils.translation module, as well as a template tag library; their detailed use is documented in the official Django documentation and unnecessary to cover here. The number one priority in your app is ensuring that user-facing strings in Python code are “wrapped” with gettext and translatable. If you do nothing else but this, you’ll have accomplished that critical 80%.

The reason this is the singular priority is twofold: one, it is entirely possible to create the necessary language files for any language given that the strings are available for lookup, and two, this is the only user-facing content that the end developer cannot change.
  • By user-facing strings, I mean here any string that would be expected to be displayed to the application user and seen in their browser. Unless an exception is used to raise a message to the end user (e.g., via a validation error), you probably don’t want to translate exception messages.

Making templates translatable is the second priority, and whether this is a close or distant second will depend entirely on the nature of templates in your Django standalone app. The reason here is that templates are entirely extendable and overridable by developer users. If your templates are sparse and fully intended to be replaced by developers, then the value of making these translatable is negligible. On the other hand, if the templates in your application are richly structured and intended to be part of the user-facing experience, then making sure these are translatable – by using the template tags from the i18n tag library – should be high priority.

With these two tasks out of the way, the necessity and value of further efforts now steeply decline unless you have known use cases in specific languages and ready resources for creating translations. Those additional steps include generating and adding po files, the text-based source files for gettext translations, integrating with a translations service, and compiling and including mo files, the binary lookup files used by gettext.

Generating and adding po files is simple enough and requires absolutely no knowledge of the target language. However, it does involve choosing a language! This is a bit like optimizing without measuring. Until you know what demand there is for specific languages, you’re in no position to make this choice. It might make it more obvious that your app is ready for translation contributions, but even this is a suspect strategy. Of the three most widely spoken languages in the Western Hemisphere, there are numerous country-specific varieties; and where translations are used, these otherwise small differences are often significant.

Model content and translations

There are several sources of user-facing content in a Django application: from the templates, from the Python code itself, and from user-controlled model-based content. In most websites and web applications with non-trivial amounts of content, model-based content makes up the bulk of the content. While you as the standalone app author are not providing this content, you can provide affordances for developer users to add translations. Of course, how you do this and whether it’s necessary or valuable depend on the nature of your standalone app.

In your own Django project, using models you control, there are several solutions available, beyond those described in the following. Third-party standalone apps like [django-modeltranslation] (https://django-modeltranslation.readthedocs.io/en/latest/registration.html) let you add locale-specific fields to existing models and access these seamless from your app with minimal intervention. However, this involves modifying database tables, which means database migrations, and in the case of third-party apps, this means trying to manage migrations for a library not under your control and moreover losing track of these migrations if you’re using any kind of ephemeral deployment system, all of which is to say that for the developer user managing a Django project, trying to add translation support to the models in third-party apps is not viable. Thankfully, there are affordances you can provide as the developer of a Django standalone app.

For a content heavy application, where a model has several or numerous fields representing user-facing content, an excellent and flexible strategy is to include a locale field and allow translations to vary by instance or, more specifically, by database row. This means that for an Email model, for instance, you might allow multiple instances with the same base:
from django.conf import settings
from django.db import models
class EmailType:
        confirmation = "confirmation"
        notification = "notification"
        @classmethod
        def choices(cls):
                return [(cls.notification, cls.notification),
                                (cls.confirmation, cls.confirmation)]
class EmailMessage(models.Model):
        email_type = models.CharField(
                max_length=20,
                choices=EmailType.choices(),
        )
        locale = models.CharField(
                max_length=10,
                choices=settings.LANGUAGES,
                default="",
        )
        message = models.TextField()
        class Meta:
                unique_together = ("email_type", "locale")

Now there’s a built-in way to include translated content in the database, without any further modification of the database. This strategy makes the most sense for “content heavy” models, though, that represent either a significant amount of content or a large number of fields that should all be translated together.

For models with only a few fields requiring translation, another option, and one that has not been pursued to much fanfare as of this writing, is to make use of built-in lookup fields. If you’re willing to commit your developer users to the PostgreSQL database, then using either the HStoreField or JSONField is an option. Both can be used to represent dictionaries; HStoreField is simpler and restricted to strings, but JSONField uses default database functionality (HStoreField requires that you install a database extension).

Taking this strategy to its maximum potential is an encouraged exercise for the reader, but at its simplest it involves storing core field data in a dictionary:
from django.contrib.postgres.fields import JSONField
from django.db import models
class Product(models.Model):
        title_source = JSONField()
        price = models.IntegerField()
        def title(self, locale=""):
                if locale:
                        try:
                                return self.title_source[locale]
                        except KeyError:
                                pass
                return self.title_source[""]

This neatly solves for the data storage problem, as well as explicit retrieval. The usability of such an interface warrants vast improvement, including for updating data and especially for the simplified querying as afforded by something like django-translation. Maybe that could be your first Django standalone app!

Summary

In this chapter, we reviewed what internationalization is and why it’s important to accommodate in your standalone app. You learned how to prioritize adding translation support to your app, when to include specific language translations for your app, and also how to approach translating model-based content.

In the next chapter, we’ll learn about the problems of managing version compatibility with different Python and Django versions and some strategies for solving these problems.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.188.11