elixir-cldr / cldr_trans

Cldr-based fork of the most excellent "trans" library
Other
10 stars 3 forks source link

feature request: pass in a custom module to handle the translation fields per locale #5

Open petrus-jvrensburg opened 2 years ago

petrus-jvrensburg commented 2 years ago

Currently, the translations macro accepts a translation_module argument, so that the developer can pass in their own module for handling translations. Such a module might look like:

defmodule MyApp.Article.Translations do
  use Ecto.Schema

  @primary_key false
  embedded_schema do
    embeds_one :fr, MyApp.Article.Translations.Fields, on_replace: :update
    embeds_one :es, MyApp.Article.Translations.Fields, on_replace: :update
  end
end

But, to me, this doesn't seem to be terribly useful. Relying on the macro to generate the embedded schema, with embedded fields for each configured locale seems easier.

However, being able to override the module that is used for the underlying MyApp.Article.Translations.Fields would be useful to me. It would allow me to add fields for implementing logic on a per-locale basis, like

Would this be a useful change for anyone else?

kipcole9 commented 2 years ago

@petrus-jvrensburg very sorry for the slow response. ElixirConf is over so I can get back to L10N now. A lot of what you propose makes a lot of sense. Let me work up some implementation ideas for your consideration over the next few days. I want to keep backwards compatibility with trans but after than, an easy developer experience and better support for "extended" translation structs are good ideas for sure!

petrus-jvrensburg commented 2 years ago

Thanks. I have since realised that if I name the embedded struct just right, then it's picked up by the code that is generated by the translations macro. But I'm sure it would be better if that were explicit.

My current approach is to use models that look like this:

defmodule MyApp.Product do
  use Ecto.Schema
  use Nohara.Cldr.Trans, translates: [:title, :description]
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "products" do
    field(:title, :string)
    field(:description, :string)
    field(:brand, :string)
    field(:image, :string)
    field(:is_active, :boolean, default: true)
    field(:price, :decimal)
    ...

    translations(:translations)
    timestamps()
  end

  def translation_locales(), do: __MODULE__.Translations.__schema__(:fields)

  def translatable_fields(), do: __MODULE__.__trans__(:fields)

  @doc false
  def changeset(product, attrs) do
    product
    |> cast(attrs, [
      :title,
      :description,
      :brand,
      :image,
      :is_active,
      :price,
      ...
    ])
    # use 'cast_embed' to handle values for the 'translations' map-field with
    # a nested changeset
    |> cast_embed(:translations, with: &translations_changeset/2)
    |> validate_required([:title])
  end

  defp translations_changeset(translations, params) do
    translations
    |> cast(params, [])
    |> then(fn tmp_changeset ->
      # use 'cast_embed' to handle values for translated fields for each of the
      # configured languages
      translation_locales()
      |> Enum.reduce(tmp_changeset, fn locale, acc ->
        cast_embed(acc, locale)
      end)
    end)
  end
end

defmodule MyApp.Product.Translations.Fields do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key false
  embedded_schema do
    for translatable_field <- MyApp.Product.translatable_fields() do
      field translatable_field, :string
    end

    field :updated_at, :naive_datetime
    field :updated_by, Ecto.UUID
  end

  @doc false
  def changeset(translation, attrs) do
    translation
    |> cast(attrs, MyApp.Product.translatable_fields() ++ [:updated_at, :updated_by])
  end
end