What is Trans?
Trans is a library that helps you managing embedded model translations. Trans is inspired by the great hstore translate gem for Ruby.
IMPORTANT: for the moment, Trans query building works only with PostgreSQL, since the queries use the special operators for JSONB. Keep this in mind if you want to find models filtering from translated attributes.
Trans is published on hex.pm. The documentation is also available online.
Why Trans?
The traditional approach to content internationalization consists of using an additional
table for each translatable model, this table contains the model translations. For example,
we may have a posts and posts_translations tables.
Trans provides a different approach based on modern RDBMSs support for unstructured data. Each translatable model can have a field (stored as a column in the database) that contains its translations in the form of a dictionary. This approach allows us to reduce table joins, specially when the number of translatable models and instances gets bigger.
Trans is lightweight and modularized. The main functionality is provided by the Trans.Translator and the Trans.QueryBuilder modules. The Trans module simplifies the calls to translator and query builder functions from a model.
How can I use Trans?
Adding translations to a model
The first step consists on adding a new column to the desired table. This column will be known as the translation container.
defmodule MyApp.Repo.Migrations.AddTranslationsColumn do
use Ecto.Migration
def change do
update table(:articles) do
add :translations, :map
end
end
endThe model's schema must be also updated, so it can be mapped by Ecto.
defmodule MyApp.Article do
use Ecto.Schema
schema "articles" do
... # Previous fields
field :title, :string
field :body, :string
field :author, :string
field :translations, :map # This field will contain our translations
end
endUsing helper functions
Trans provides two kind of helper functions:
-
Content translation accessors, provided by the
Trans.Translatormodule. -
Helpers for query construction, provided by the
Trans.QueryBuildermodule.
The functions provided by those two modules can be used with any model.
If a certain model has some special configuration (for example, the translation container
field is named translations_container instead of simply translations) it may be
tiresome to manually specify this on every call. To avoid this unnecesary repetition,
we can use the Trans module, which provides a nice way of specifying default options
that will be automatically passed to Trans.Translator and Trans.QueryBuilder.
You can use the Trans module in your model like this:
defmodule MyApp.Article do
# ...
use Trans, translates: [:title, :body], defaults: [container: :translations]
# ...
endWe must define the list of translatable fields for the model, otherwise Trans will raise an error during compilation.
We can also provide a list of default options that will be automatically passed
in the convenience functions. In the example, we are specifying the translation
container of the model (by default Trans looks for a container called translations
so we could omit it in the example).
Storing translations
Translations are stored as a map of maps in the translation container field. For example
translations = %{
"es" => %{"title" => "¿Por qué Trans es genial?", "body" => "Disertación sobre la genialidad de Trans"},
"fr" => %{"title" => "Pourquoi Trans est grande?", "body" => "Dissertation sur le génie de Trans"}
}
changeset = Article.changeset(%Article{}, %{
title: "Why Trans is great",
body: "An explanation about the Trans greatness",
author: "Cristian Álvarez Belaustegui",
translations: translations
})
article = Repo.insert!(changeset)
Querying translations
We may need to get articles that are translated into a certain language. To do this we may
use the Trans.QueryBuilder.with_translations/3 function (or the helper provided by Trans in our model).
articles_translated_to_spanish = Article |> Article.with_translations(:es) |> Repo.all
# SELECT a0."id", a0."title", a0."body", a0."translations", a0."author" FROM "articles" AS a0 WHERE (a0."translations"->>$1) is not null) ["es"] OK query=17.1ms queue=0.1msWe may also want to get articles for which their french title contains "Trans".
articles = Article |> Article.with_translation(:fr, :title, "%Trans%", type: :like)
# [debug] SELECT a0."id", a0."title", a0."body", a0."translations", a0."author" FROM "articles" AS a0 WHERE (a0."translations"->$1->>$2 LIKE $3) ["fr", "title", "%Trans%"] OK query=2.1ms queue=0.1ms
The Trans.QueryBuilder.with_translation/5 function supports three types of comparisons:
- If no type is specified, the query will look for an exact match.
-
For a case-sensitive pattern comparison use
type: :like -
For a case-insensitive pattern comparison use
type: :ilike
Translating fields
When we have a model struct, we can use the Trans.Translator.translate/4 (or the equivalent helper provided by Trans) function to easily load
a certain translation.
Article.translate(article, :es, :body) # "Disertación sobre la genialidad de Trans"
The Trans.Translator.translate/3 function also provides a fallback mechanism for when
non existant translations are accessed:
Article.translate(article, :de, :title) # Fallback to untranslated value: "Why Trans is great"Since the translation container is a simple map, we can always access its values manually:
article.translations["es"]["body"] # "Disertación sobre la genialidad de Trans"