How string interpolation is done within the error messages of changesets (using %{})?

Hello everybody,

When I look at what an error looks like in a Changeset, it’s something like this:

[username: {"should be at most %{count} character(s)", [count: 20, validation: :length, kind: :max, type: :string]}]
# which renders to
# "should be at most 20 character(s) max"

It seems that there is some kind of templating with interpolation done inside %{keyword} as soon as the keyword is present as a key in the given list of keywords…

I don’t think it’s something native to elixir like printf in C (or I never heard of on my learning path) but more a convenience or a syntactic sugar provided by Phoenix…

So I wanted to know where this happens?

This behavior seems to be related to the Gettext module but when I follow down the path it seems (at least to me btw) overly complicated with functions with 6 to 8 parameters with odd names (dngettext, dpngettext etc.)

And I expected to find calls to String.replace or Regex.replace in order to replace %{} but I don’t find anything…

Does anyone have any clues?

Thank you…

1 Like

Looks like the suggestion is to use Changeset.traverse_errors to get the errors in the format you want.

According to Elixir School:

You may be surprised that the error message contains the cryptic %{count} — this is to aid translation to other languages; if you want to display the errors to the user directly, you can make them human readable using traverse_errors/2 — take a look at the example provided in the docs.

If you are using Phoenix, you can see how this error format is managed for you using Gettext in the file web/views/error_helpers.ex.

Update:
There is a nice example on how to use the traverse_errors function to replace the values yourself:

{"should be at least %{count} characters", [count: 3, validation: :length, min: 3]}

iex> traverse_errors(changeset, fn {msg, opts} ->
      ...>   Enum.reduce(opts, msg, fn {key, value}, acc ->
      ...>     String.replace(acc, "%{#{key}}", to_string(value))
      ...>   end)
      ...> end)
      %{title: ["should be at least 3 characters"]}
2 Likes

Hello…

I should have been more clear… Indeed, using error_tag from views/error_helper.ex actually display the message in the expected formatted output…

I just wanted to learn where that non-standard elixir %{} interpolation is happening…

This is exactly what I was expecting to find somewhere in the code base (as explained above). The interpolation implementation using String.replace or Regex.replace
But this one is an example from the comments/docs…

So I still wonder where that interpolation is happening… But at least I know how it’s done… Thank you…

This seems to be happening in Gettext here and here but I have to admit that I don’t really understand what is happening. Perhaps someone else can shed some light :slight_smile:

3 Likes

Oh, yes, now I understand your question!

And yes, it’s a very complicated code.

Looks like what you are looking for is happening in Gettext.Interpolation

2 Likes

Wow… I wasn’t expecting a Rube Goldberg machine-like implementation…
I wonder why it’s implemented like this while they gave (more intelligible) example using Regex.replace as pointed out above…
I guess it’s about performance (maybe related to how Elixir immutability is optimized with linked list?), though I would like to have the confirmation from experts about it.

Thanks! One minute too late :wink:

1 Like

This is common pattern in Erlang (and by extension - Elixir) that you use “quasi” io lists for that. So you have type like template() :: maybe_improper_list(template() | iolist()) and then you can easily traverse it and replace all atoms with proper values (while building another io list). So this “Goldberg machine” does exactly that - first compile all the strings into simpler forms and then replacing all needed values within the io list.

3 Likes

Thanks for the explanations… It does make total sense.

1 Like