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…
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"]}
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
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.
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.