Current section

Files

Jump to
trans lib trans.ex
Raw

lib/trans.ex

defmodule Trans do
@moduledoc """
Trans provides a way to manage and query translations embedded into schemas
and removes the necessity of maintaing extra tables only for translation storage.
Trans is split into 2 main components:
* `Trans.Translator` - provides functions to easily access translated values
from schemas and fallback to a default locale when the translation does not
exist in the required one. **If you want to get translations from a schema you
should take a look at this module**.
* `Trans.QueryBuilder` - provides functions that can be chained in queries and
allow filtering by translated values. **If you want to filter queries using
translated fields you should take a look at this module**. To use this module
you must have `Ecto` among your dependencies.
## What does this package do?
`Trans` allows you to store translations of a Struct as an extra field of
that Struct.
`Trans` sees its main utility when it is used on `Ecto.Schema` modules.
**When paired with an `Ecto.Schema`, `Trans` allows you to keep the schema and
its translations on the same database table.** This removes the necessity of
spreading the schema and its translations on multiple tables and reduces the
number of required *JOINs* that must be performed on queries.
The field that stores the translations on the Struct is called the *translation
container* and by default it is expected to be named `translations`, but
that can be easily overriden.
## What does this module do?
Although it does not provide any functions that can be used directly
(those functions are provided by the `Trans.Translator` and `Trans.QueryBuilder`
modules), using the `Trans` module provides two main benefits:
- **Checking the safety of the translation operations** - When your schema uses
`Trans` it will safe check the queries that filter on a translated field. This
means that trying to set a translation filter for an untranslated field will
produce a error when building the query.
- **Avoiding the repetition of default options** - If your schema's
*translation container* receives a different name than the default
`translations`, it would have to be specified on every call to
`Trans.QueryBuilder` or `Trans.Translator` functions. When using the `Trans`
module, this setting can be specified once an applied automatically when required.
## Usage examples
The general way to use `Trans` in a module is:
use Trans, translates: [:field_1, :field_2][, defaults: [...]]
Suppose that you have an `Article` schema that has a title and a body that must
be translated. You can set up the convenience functions in your module
like the following example:
defmodule Article do
use Ecto.Schema
use Trans, translates: [:title, :body]
schema "articles" do
field :title, :string
field :body, :string
field :translations, :map
end
end
Now imagine that the `Article` schema uses a different *translation container*
to store the translations. This should be specified when using `Trans` so
it can automatically provide the required defaults to `Trans.QueryBuilder`
and `Trans.Translator` like the following example:
defmodule Article do
use Ecto.Schema
use Trans, defaults: [container: :article_translations],
translates: [:title, :body]
schema "articles" do
field :title, :string
field :body, :string
field :article_translations, :map
end
end
"""
defmacro __using__(opts) do
setup_convenience_functions(get_defaults(opts), get_translatables(opts))
end
defp setup_convenience_functions(defaults, translatables) do
quote do
if Code.ensure_compiled?(Trans.QueryBuilder) do
# Wrapper of Trans.QueryBuilder.with_translations/3
def with_translations(query, locale, opts \\ []) do
Trans.QueryBuilder.with_translations(query, locale, opts ++ unquote(defaults))
end
# Wrapper of Trans.QueryBuilder.with_translations
def with_translation(query, locale, field, expected, opts \\ []) do
if not Enum.member?(unquote(translatables), field) do
raise ArgumentError, "The field `#{field}` is not declared as translatable"
end
Trans.QueryBuilder.with_translation(query, locale, field, expected, opts ++ unquote(defaults))
end
end
end
end
defp get_translatables(opts) do
case List.keyfind(opts, :translates, 0) do
nil -> raise ArgumentError, "You must provide a `translates` option with a list of the translatable fields"
{:translates, translatables} when is_list(translatables) -> translatables
{:translates, translatables} -> [translatables]
end
end
defp get_defaults(opts) do
case List.keyfind(opts, :defaults, 0) do
nil -> []
{:defaults, defaults} -> defaults
end
end
end