I18n Helpers - ease the use of embedded translations in Ecto Schemas

Hello everyone :wave:

I’ve been learning Elixir for a few months now and just published my first Hex package :tada:
Any feedback is welcome.

This library might be relevant to you if you need to store translations in the database (e.g. user-generated content), and want to store these translations in a JSON data type (as opposed to storing them in separate tables).

You can manage that by just using the Ecto’s :map field type, but the idea of this library is to provide a set of helpers to ease working with such embedded translations.

The library expects you to store a map of translations for each field. For example:

%Post{
  title: %{"en" => "The title", "fr" => "Le titre"},   
  body: %{"en" => "The content", "fr" => "Le contenu"}
}

In brief, the library helps you translating your Ecto struct and its associations for a given locale (in one call), with a fallback locale and a handler for missing translations.

post =
 Repo.all(Post)
 |> Repo.preload(:category)
 |> Repo.preload(:comments)
 |> Translator.translate("fr", fallback_locale: "en")

assert post.translated_title == "Le titre"
assert post.category.translated_name == "La catégorie"
assert List.first(post.comments).translated_text == "Un commentaire"

As you might already understand from the example above, by “translating an Ecto struct”, it is meant extracting the texts (from the different translations’ maps) for a given locale and fallback locale, and set them in a virtual field (which is named by the field it retrieved the text from prefixed by “translated_”).

It is done recursively for the struct and all their loaded associations.

It is particularly useful if you need to use different fallback locales throughout your application as is my case, or if you need to be notified for missing translations.

This library differs from other similar libraries where:

  • translations are stored in a JSON data type (in Postgres that will be jsonb type);
  • each field has its translations map (as opposed to one big translation map for all fields);
  • there is no notion of “main” locale, all the texts are stored in the map (but you can specify a fallback locale);
  • translations for a given locale are retrieved in one call for an Ecto struct and all its associations;
  • a fallback locale and a handler for missing translations may be provided.

:v:

2 Likes

It’s indeed very useful for setups where you want to keep the DB schema rather simple. How would one query the DB for e.g. a search feature? I guess it’s possible to search inside JSON using Postgres, I haven’t done that yet, though.

Maybe you could provide a helper function that assembles the correct Ecto query part?

1 Like

It would look something like:

from(
  p in Post,
  where: ilike(fragment("(?->>'en')", p.title), "%elixir%"),
  select: p
)

Thank you for the suggestion; I think indeed a helper to build Ecto queries should be part of the library :+1: It’s not my priority right now as I don’t need a search feature in my project.

However, I’m now working on view helpers to make working with form and such embedded fields easier.
These view helpers would render something like:

chrome_2019-09-25_12-08-56

These fields can be generated in one line.

This will be push’ed in a few days.

2 Likes