Current section
Files
Jump to
Current section
Files
lib/ecto_i18n.ex
defmodule EctoI18n do
@moduledoc """
Provides i18n support for Ecto.
## Preface
There're [lots of strategies to localize contents in database](https://dejimata.com/2017/3/3/translating-with-mobility)
([archived](https://web.archive.org/web/20240528023514/https://dejimata.com/2017/3/3/translating-with-mobility)).
For now, `#{inspect(__MODULE__)}` implements only strategy 6 mentioned
above - creating an extra column for storing all the localized data
for that table.
With this strategy, it can:
* avoid using extra tables for storing localized data.
* avoid using complex JOINs when retrieving localized data.
> Maybe other strategies will be implemented later, but for now, I only
> need this one.
## Quick start
Let's say that we have a schema which needs to be localized:
defmodule MyApp.Shop.Product do
use Ecto.Schema
schema "products" do
field :sku, :string
field :name, :string
end
end
The first step is to add a new column to the table at database level, so
we can store localized data in it:
defmodule MyApp.Repo.Migrations.AddLocalesToProducts do
use Ecto.Migration
def change do
alter table(:products) do
add :name_i18n, :map
end
end
end
The second step is to update schema for using the new column:
defmodule MyApp.Shop.Product do
use Ecto.Schema
use EctoI18n.Schema, locales: ["en", "zh-Hans"]
schema "products" do
field :sku, :string
field_i18n :name, :string
end
end
> If you're curious about the underlying implementation here, you can read
> `EctoI18n.Schema` to learn more.
Next, you can use the extensions provided by `#{inspect(__MODULE__)}` to
work with the localized schema, such as:
* `EctoI18n.i18n_support?/1` / `EctoI18n.i18n_support?/2`
* `EctoI18n.locales/1`
* `EctoI18n.localize!/2`
* `EctoI18n.Changeset.cast_i18n/2` / `EctoI18n.Changeset.cast_i18n/3`
* `EctoI18n.Query` (Still in planning)
* ...
"""
@type locale :: atom() | binary()
@doc """
Checks whether a module or a struct has i18n support.
## Examples
iex> EctoI18n.localizable?(Product)
iex> EctoI18n.localizable?(%Product{})
"""
@spec i18n_support?(module() | struct()) :: boolean()
def i18n_support?(module_or_struct)
def i18n_support?(module) when is_atom(module) do
{:__ecto_i18n__, 1} in module.__info__(:functions)
end
def i18n_support?(struct) when is_struct(struct) do
module = struct.__struct__
i18n_support?(module)
end
@spec i18n_support?(module() | struct(), atom()) :: boolean()
def i18n_support?(module_or_struct, field)
def i18n_support?(module, field) when is_atom(module) do
i18n_support?(module) && field in module.__ecto_i18n__(:fields)
end
def i18n_support?(struct, field) when is_struct(struct) do
module = struct.__struct__
i18n_support?(module, field)
end
@doc """
Returns supported locales of a struct or the underlying module.
"""
@spec locales(module() | struct()) :: [locale()]
def locales(module_or_struct)
def locales(module) when is_atom(module) do
unless i18n_support?(module) do
raise RuntimeError,
"`#{inspect(module)}` module doesn't have i18n support"
end
module.__ecto_i18n__(:locales)
end
def locales(struct) when is_struct(struct) do
module = struct.__struct__
locales(module)
end
@doc """
Localizes a struct with given locale recursively.
All localizable values in the struct will be localized into the give locale.
## Examples
iex> EctoI18n.localize!(product, "zh-Hans")
"""
@spec localize!(struct(), locale()) :: struct()
def localize!(struct, locale) when is_struct(struct) and is_atom(locale),
do: do_localize!(struct, locale)
def localize!(struct, locale) when is_struct(struct) and is_binary(locale),
do: do_localize!(struct, String.to_atom(locale))
defp do_localize!(%Ecto.Association.NotLoaded{} = term, _locale), do: term
defp do_localize!(%{__meta__: _} = struct, locale) when is_struct(struct) do
module = struct.__struct__
struct =
if EctoI18n.i18n_support?(module) do
mappings = module.__ecto_i18n__(:mappings)
Enum.reduce(mappings, struct, fn {field, i18n_field}, acc ->
value = struct |> Map.fetch!(i18n_field) |> Map.fetch!(locale)
Kernel.struct(acc, [{field, value}])
end)
else
struct
end
associations = module.__schema__(:associations)
embeds = module.__schema__(:embeds)
assocs = associations ++ embeds
Enum.reduce(assocs, struct, fn assoc, struct ->
Map.update!(struct, assoc, &do_localize!(&1, locale))
end)
end
defp do_localize!(struct, locale) when is_struct(struct) do
keys = Map.keys(struct) -- [:__struct__]
Enum.reduce(keys, struct, fn key, struct ->
Map.update!(struct, key, &do_localize!(&1, locale))
end)
end
defp do_localize!(map, locale) when is_map(map) do
Enum.into(map, %{}, fn {k, v} -> {k, do_localize!(v, locale)} end)
end
defp do_localize!(list, locale) when is_list(list) do
Enum.map(list, fn term -> do_localize!(term, locale) end)
end
defp do_localize!(term, _locale), do: term
end