Multilingual database content

One feature of Lino is its built-in support for single-table multilingual database content. This tutorial explains what it is.

Note that we are not talking about Internationalization (i18n) here. Internationalization is when the front end can speak different languages. Lino has nothing to add to the existing Django techniques about Internationalization, that's why we deliberately didn't add lino.modlib.users and front end translation in this tutorial.

When to use BabelFields

Imagine a Canadian company which wants to print catalogs and price offers in an English and a French version, depending on the customer's preferred language. They don't want to maintain different product tables because it is one company, one accounting, and prices are the same in French and in English. They need a Products table like this:

Designation (en)

Designation (fr)

Category

Price

ID

Chair

Chaise

Accessories

29.95

1

Table

Table

Accessories

89.95

2

Monitor

Écran

Hardware

19.95

3

Mouse

Souris

Accessories

2.95

4

Keyboard

Clavier

Accessories

4.95

5

Now imagine that your application is being used not only in Canada but also in the United States. Your US customers don't want to have a "useless" column for the French designation of their products.

This is where you want multi-lingual database content. In that case you would simply

  • use BabelCharField instead of Django's CharField for every translatable field and

  • set the languages attribute to "en" for your US customer and to "en fr" for your Canadian customer.

An example

If you have installed a contributor environment (see Setting up a Lino contributor environment), then you can run the following examples on your computer.

Go to lino_book.projects.mldbc:

$ go mldbc

Make sure that the demo database is initialized:

$ python manage.py prep

Using the shell

Now open the interactive Django shell:

$ python manage.py shell

You can print a catalog in different languages:

>>> print(', '.join([str(p) for p in Product.objects.all()]))
Chair, Table, Monitor, Mouse, Keyboard, Consultation
>>> from django.utils import translation
>>> with translation.override('fr'):
...     print(', '.join([str(p) for p in Product.objects.all()]))
Chaise, Table, Ecran, Souris, Clavier, Consultation

Here is how we got the above table:

>>> from lino.api import rt
>>> rt.show(mldbc.Products)
==================== ================== ============= ============
 Designation          Designation (fr)   Category      Price
-------------------- ------------------ ------------- ------------
 Chair                Chaise             Accessories   29,95
 Table                Table              Accessories   89,95
 Monitor              Ecran              Hardware      19,95
 Mouse                Souris             Accessories   2,95
 Keyboard             Clavier            Accessories   4,95
 Consultation         Consultation       Service       59,95
 **Total (6 rows)**                                    **207,70**
==================== ================== ============= ============

Using the web interface

$ go mldbc
$ python manage.py prep
$ python manage.py runserver
Analyzing Tables...
Analyze 0 slave tables...
Discovering choosers for model fields...
Analyzing Tables...
Analyze 0 slave tables...
Discovering choosers for model fields...
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
November 05, 2019 - 07:32:06
Django version 2.2.6, using settings 'lino_book.projects.mldbc.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Screenshots

The screenshots on the left have been taken on a server with languages = ['en'], those on the right on a server with languages = ['de','fr'].

../../_images/babel1a.jpg ../../_images/babel1b.jpg ../../_images/babel2a.jpg ../../_images/babel2b.jpg ../../_images/babel3a.jpg ../../_images/babel3b.jpg

The settings.py file

from lino.projects.std.settings import *

class Site(Site):

    title = "MLDBC Tutorial"

    demo_fixtures = ['demo']

    languages = 'en fr'

    def get_installed_apps(self):
        yield super(Site, self).get_installed_apps()
        # yield 'lino.modlib.system'
        yield 'lino_book.projects.mldbc'

    def setup_menu(self, user_type, main):
        m = main.add_menu("products", "Products")
        m.add_action('mldbc.Products')
        super(Site, self).setup_menu(user_type, main)
        
SITE = Site(globals())

DEBUG = True

This is where you specify the languages setting.

The models.py file

from lino.api import dd
from lino import mixins

from django.utils.translation import ugettext_lazy as _


class Categories(dd.ChoiceList):
    verbose_name = _("Category")
    verbose_name_plural = _("Categories")

Categories.add_item("01", _("Hardware"), 'hardware')
Categories.add_item("02", _("Service"), 'service')
Categories.add_item("03", _("Accessories"), 'accessories')
Categories.add_item("04", _("Software"), 'software')


class Product(mixins.BabelNamed):

    price = dd.PriceField(_("Price"), blank=True, null=True)

    category = Categories.field(blank=True, null=True)

    class Meta:
        verbose_name = 'Product'
        verbose_name_plural = 'Products'


class Products(dd.Table):
    model = Product
    sort_order = ['name']
    column_names = "name category price *"
    auto_fit_column_widths = True

    detail_layout = """
    id price category
    name
    """

    insert_layout = """
    name
    category price
    """


Note that this is the first time we use a dd.ChoiceList they deserve another tutorial on their own.

The demo fixture

# -*- coding: UTF-8 -*-

from __future__ import unicode_literals

from lino.api import dd

Product = dd.resolve_model('mldbc.Product')


def P(en, de, fr, cat, price):
    return Product(
        price=price,
        category=cat,
        **dd.babel_values('name', en=en, de=de, fr=fr))


def objects():
    yield P("Chair", "Stuhl", "Chaise", '03', '29.95')
    yield P("Table", "Tisch", "Table", '03', '89.95')
    # doctests fail with non-ascii text, so we need to cheat:
    # yield P("Monitor", "Bildschirm", "Écran", '01', '19.95')
    yield P("Monitor", "Bildschirm", "Ecran", '01', '19.95')
    yield P("Mouse", "Maus", "Souris", '03', '2.95')
    yield P("Keyboard", "Tastatur", "Clavier", '03', '4.95')
    yield P("Consultation", "Beratung", "Consultation", '02', '59.95')

Note how the application developer doesn't know which languages will be set at runtime.

Of course the fixture above supposes a single person who knows all the languages, but that's just because we are simplifying. In reality you can do it as sophisticated as you want, reading the content from different sources.

BabelFields and migrations

BabelFields cause the database structure to change when a site maintainer locally changes the languages setting of a Lino site.

That's why the application carrier cannot provide Django migrations for their product. See Data migrations à la Lino and Django migrations on a Lino site.