Idea: intermediary macro to remove boilerplate and get cleaner api
Closed this issue · 3 comments
There is a lot of boilerplate and duplicated code needed with the current version of Trans. For example, you are defining the translated fields three times (use statement, fields(), Translation.Fields).
We are solving most of those things with an intermediary macro. I understand this is probably not one size fits it all, but maybe there is some inspiration for a future version and other users.
We ended up with something like this.
defmodule Marko.Schema do
use Marko, :typed_schema
@required_fields [:key]
@optional_fields [:this]
typed_schema "categories" do
field(:key, :string)
field(:this, :string)
translate_field(:foo)
translate_field(:bar)
embed_translations()
timestamps(type: :naive_datetime_usec)
end
@spec changeset(schema :: t, params :: Marko.params()) :: Changeset.t()
def changeset(schema, params \\ %{}) do
schema
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> cast_translations()
end
And the magic happens here
defmodule Marko.Translation do
@moduledoc """
Translation are based on https://github.com/crbelaus/trans
This module handles a lot of the needed boilerplate and allows for a cleaner api.
To enable translation:
- call `translate_field(:foo)` in the schema
- call `embed_translations()` in the schema
- call `cast_translations()` in the changeset
- create a database field `add(:translations, :map, null: false)`
"""
import Ecto.Changeset, only: [cast: 3, cast_embed: 2, cast_embed: 3]
@spec trans :: Macro.t()
def trans() do
quote do
# We use the module attribute `:translated_fields` as storage for our macro
Module.register_attribute(__MODULE__, :translated_fields, persist: true, accumulate: true)
import Marko.Translation, only: [translate_field: 1, cast_translations: 1, embed_translations: 0]
@before_compile Marko.Translation
end
end
defmacro __before_compile__(env) do
case Module.get_attribute(env.module, :translated_fields) do
[] ->
nil
fields ->
quote do
use Trans, translates: unquote(fields)
defmodule Translations.Fields do
use Marko, :typed_schema
@required_fields unquote(fields)
@primary_key false
typed_embedded_schema do
for f <- unquote(fields) do
field(f, :string)
end
end
@spec changeset(schema :: t, params :: Marko.params()) :: Ecto.Changeset.t(t)
def changeset(schema, params \\ %{}) do
schema
|> cast(params, @required_fields)
|> validate_required(@required_fields)
end
end
end
end
end
defmacro translate_field(field) do
quote do
@translated_fields unquote(field)
field(unquote(field), :string)
end
end
defmacro embed_translations() do
quote do
Ecto.Schema.embeds_one :translations, Translations, on_replace: :update, primary_key: false do
for locale <- MarkoCldr.known_locale_names() do
Ecto.Schema.embeds_one(locale, __MODULE__.Fields, on_replace: :update)
end
end
end
end
@spec cast_translations(Ecto.Changeset.t()) :: Ecto.Changeset.t()
def cast_translations(changeset) do
cast_embed(changeset, :translations,
required: true,
with: fn schema, params ->
changeset = cast(schema, params, [])
Enum.reduce(MarkoCldr.known_locale_names(), changeset, &cast_embed(&2, &1))
end
)
end
end
Hi @rubas first of all thank for your interest in Trans!
This looks incredibly useful and a PR would be very welcome, this would be a great addition to Trans and I believe would be useful to others as well.
Thanks for the feedback.
There are some specific implementation details included. For example we use TypedEctoSchema and always expect translations for all defined locales (MarkoCldr.known_locale_names()
).
This is not a one size fits all solution.
This is now supported by the new translations/2 macro provided by the just released Trans 3.0.0.