Current section
Files
Jump to
Current section
Files
README.md
# Trans[](https://circleci.com/gh/crbelaus/trans/tree/master)[](https://hex.pm/packages/trans)`Trans` provides a way to manage and query translations embedded into schemasand removes the necessity of maintaing extra tables only for translation storage.It is inspired by the great [hstore translate](https://rubygems.org/gems/hstore_translate)gem for Ruby.`Trans` is published on [hex.pm](https://hex.pm/packages/trans) and the documentationis also [available online](https://hexdocs.pm/trans/). Source code is available in this samerepository under the Apache2 License.On April 17th, 2017, `Trans` was [featured in HackerNoon](https://hackernoon.com/introducing-trans2-407610887068)## Optional RequirementsHaving Ecto SQL and Postgrex in your application will allow you to use the `Trans.QueryBuilder`component to generate database queries based on translated data. You can stilluse the `Trans.Translator` component without those dependencies though.- [Ecto SQL](https://hex.pm/packages/ecto_sql) 3.0 or higher- [PostgreSQL](https://hex.pm/packages/postgrex) 9.4 or higher (since `Trans` leverages the JSONB datatype)Support for MySQL JSON type (introduced in MySQL 5.7) will come also, but rightnow it is not yet implemented at the database adapter level.## Why Trans?The traditional approach to content internationalization consists on using anadditional table for each translatable schema. This table works only as a storagefor the original schema translations. For example, we may have a `posts` anda `posts_translations` tables.This approach has a few disadvantages:- It complicates the database schema because it creates extra tables that are coupled to the "main" ones.- It makes migrations and schemas more complicated, since we always have to keep the two tables in sync.- It requires constant JOINs in order to filter or fetch records along with their translations.The approach used by `Trans` is based on modern RDBMSs support for unstructureddatatypes. Instead of storing the translations in a different table, eachtranslatable schema has an extra column that contains all of its translations.This approach drastically reduces the number of required JOINs when filtering orfetching records.`Trans` is lightweight and modularized. The `Trans` module provides metadatathat is used by the `Trans.Translator` and `Trans.QueryBuilder` modules, whichimplement the main functionality of this library.## Making a schema translatableEvery translatable schema needs a field in which the translations are stored.This field is known as the *translation container*.The first step consists on adding a new column to the schema's table:```elixirdefmodule MyApp.Repo.Migrations.AddTranslationsToArticles do use Ecto.Migration def change do alter table(:articles) do add :translations, :map end endend```The schema must be also updated, so the new column can be automatically mappedby `Ecto`.```elixirdefmodule Article do use Ecto.Schema schema "articles" do field :title, :string # our previous fields... field :body, :string # our previous fields... field :translations, :map # this is our translation container endend```Then we must use `Trans` from our schema module to indicate which fields willbe translated.```elixirdefmodule Article do use Ecto.Schema use Trans, translates: [:title, :body] schema "articles" do field :title, :string field :body, :string field :translations, :map endend```## Storing translationsTranslations are stored as a map of maps in the *translation container* of ourschema. For example:```elixiriex> changeset = Article.changeset(%Article{}, %{...> title: "How to Write a Spelling Corrector",...> body: "A wonderful article by Peter Norvig",...> translations: %{...> "es" => %{...> title: "Cómo escribir un corrector ortográfico",...> body: "Un artículo maravilloso de Peter Norvig"...> },...> "fr" => %{...> title: "Comment écrire un correcteur orthographique",...> body: "Un merveilleux article de Peter Norvig"...> }...> }...> })iex> article = Repo.insert!(changeset)```## Filtering queries by translationsWe may want to fetch articles that are translated into a certain language. Todo this we use the `Trans.QueryBuilder.translated/3` macro, which generates therequired SQL fragment for us.```elixiriex> Repo.all(from a in Article,...> where: not is_nil(translated(Article, a, :es)))# SELECT a0."id", a0."title", a0."body", a0."translations"# FROM "articles" AS a0# WHERE (NOT ((a0."translations"->"es") IS NULL))```We can also get more specific and fetch only those articles for which theirSpanish title matches "Elixir".```elixiriex> Repo.all(from a in Article,...> where: translated(Article, a.title, :es) == "Elixir")# SELECT a0."id", a0."title", a0."body", a0."translations"# FROM "articles" AS a0# WHERE ((a0."translations"->"fr"->>"title") = "Elixir")```The SQL fragment generated by the `Trans.QueryBuilder.translated/3` macro iscompatible with the rest of functions and macros provided by `Ecto.Query` and`Ecto.Query.Api`.```elixiriex> Repo.all(from a in Article,...> where: ilike(translated(Article, a.body, :es), "%elixir%"))# SELECT a0."id", a0."title", a0."body", a0."translations"# FROM "articles" AS a0# WHERE ((a0."translations"->"es"->>"body") ILIKE "%elixir%")```More complex queries such as adding conditions to joined schemas can be easilygenerated in the same way. Take a look at the documentation and tests for moreexamples.## Obtaining translations from a structIn those examples we will be referring to this article:```elixiriex> article = %Article{...> title: "How to Write a Spelling Corrector",...> body: "A wonderful article by Peter Norvig",...> translations: %{...> "es" => %{...> title: "Cómo escribir un corrector ortográfico",...> body: "Un artículo maravilloso de Peter Norvig"...> },...> "fr" => %{...> title: "Comment écrire un correcteur orthographique",...> body: "Un merveilleux article de Peter Norvig"...> }...> }...> }```Once we have already loaded a struct, we may use the `Trans.Translator.translate/3`function to easily access a translation of a certain field. Locale can be passed asan atom or a string```elixiriex> Trans.Translator.translate(article, :title, :es)"Cómo escribir un corrector ortográfico"``````elixiriex> Trans.Translator.translate(article, :title, "es")"Cómo escribir un corrector ortográfico"```or```elixiriex> Trans.Translator.translate(article, :title, Gettext.get_locale())"Cómo escribir un corrector ortográfico"```The `Trans.Translator.translate/3` function also provides a fallback mechanismthat activates when the required translation does not exist:```elixiriex> Trans.Translator.translate(article, :title, :de)"How to Write a Spelling Corrector"```## Using a different *translation container*In the previous examples we have used `translations` as the name of the*translation container* and `Trans` looks automatically for translations into thisfield.We can also give the *translation container* a different name, for example**article_translations**:```elixirdefmodule Article do use Ecto.Schema use Trans, translates: [:title, :body], container: :article_translations schema "articles" do field :title, :string field :body, :string field :article_translations, :map endend```We can call the same functions as in previous examples and both `Trans.Translator`and `Trans.QueryBuilder` will automatically look for translations in the correct field.```elixiriex> Repo.all(from a in Article,...> where: not is_nil(translated(Article, a, :es)))# SELECT a0."id", a0."title", a0."body", a0."article_translations"# FROM "articles" AS a0# WHERE (NOT ((a0."article_translations"->"es") IS NULL))```