Installation
The package can be installed by adding localize_translate to your list of dependencies in mix.exs:
def deps do
[
{:localize_translate, "~> 0.1"}
]
endDocumentation can be found at https://hexdocs.pm/localize_translate.
Attribution
localize_translate derives from trans by @crbelaus and its CLDR-integrated fork ex_cldr_trans. It is the continuation of that work within the localize ecosystem, built on :localize for CLDR-aware locale validation and parent-chain fallback.
Introduction
localize_translate provides a way to manage and query translations embedded into schemas and removes the necessity of maintaining extra tables only for translation storage.
Optional Requirements
Having Ecto SQL and Postgrex in your application will allow you to use the Localize.Translate.QueryBuilder component to generate database queries based on translated data. The runtime Localize.Translate.translate/2,3 functions work without those dependencies.
- Ecto SQL 3.0 or higher
- PostgreSQL 9.4 or higher (since
Localize.Translateleverages the JSONB datatype)
Why Localize Translate?
The traditional approach to content internationalization consists on using an additional table for each translatable schema. This table works only as a storage for the original schema translations. For example, we may have a posts and a 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 Localize.Translate is based on modern RDBMSs support for unstructured datatypes. Instead of storing the translations in a different table, each translatable schema has an extra column that contains all of its translations. This approach drastically reduces the number of required JOINs when filtering or fetching records.
Localize.Translate is lightweight and modularized. The Localize.Translate module provides the use macro for declaring translatable schemas, the runtime translate/2,3 functions, and field reflection. Localize.Translate.QueryBuilder provides the Ecto.Query macros for filtering and selecting translated values in SQL.
Quickstart
Imagine that we have an Article schema that we want to translate:
defmodule MyApp.Article do
use Ecto.Schema
schema "articles" do
field :title, :string
field :body, :string
end
endAdd a JSON column
The first step would be to add a new JSON column to the table so we can store the translations in it.
defmodule MyApp.Repo.Migrations.AddTranslationsToArticles do
use Ecto.Migration
def change do
alter table(:articles) do
add :translations, :map
end
end
endGenerate database function migration
localize_translate defines a Postgres database function to support in-db field translation. A migration task is provided to generate the migration required to define this function.
% MIX_ENV=test mix localize.translate.gen.translate_function
* creating priv/repo/migrations
* creating priv/repo/migrations/20220307212312_localize_translate_gen_translate_function.exsRun migrations
Migrate the database to add the translations column and define the database function.
% mix ecto.migrateAdd translations to schema
Once we have the new database column, update the Article schema to declare translatable fields and the configured locales:
defmodule MyApp.Article do
use Ecto.Schema
use Localize.Translate,
translates: [:title, :body],
locales: [:en, :es, :fr],
default_locale: :en
schema "articles" do
field :title, :string
field :body, :string
# use the 'translations' macro to set up a map-field with a set of nested
# structs to handle translation values for each configured locale and each
# translatable field
translations :translations
end
endThe :default_locale field stores its values in the main schema columns; only the other locales get embedded translation fields.
Casting translations
localize_translate will generate a simple default changeset for the translations field. It looks like this:
def changeset(fields, params) do
fields
|> cast(params, list_of_translatable_fields)
|> validate_required(list_of_translatable_fields)
endThat may not be flexible enough for all requirements. A custom changeset can be defined for the translations field:
def changeset(article, params \\ %{}) do
article
|> cast(params, [:title, :body])
|> cast_embed(:translations, with: &translations_changeset/2)
|> validate_required([:title, :body])
end
defp translations_changeset(translations, params) do
translations
|> cast(params, [])
|> cast_embed(:es)
|> cast_embed(:fr)
end
endQuery Building
After the schema is configured, use Localize.Translate.translate/2,3 to fetch translations and Localize.Translate.QueryBuilder to query them:
# Translate a single field, with fallback chain
Localize.Translate.translate(article, :title, [:de, :es])
# Translate the whole struct (and its embeds/associations) into Spanish
Localize.Translate.translate(article, :es)
# Filter on a translation in a query
from a in Article,
where: translated(Article, a.title, :fr) == "Elixir"Locales are validated via Localize.validate_locale/1, so atoms (:en), strings ("en"), and Localize.LanguageTag structs are all accepted. Fallback chains follow CLDR parent locales automatically — for example, Localize.Translate.translate(article, :title, Localize.LanguageTag.new!("en-AU")) walks :"en-AU" → :"en-001" → :en.