Translate Ecto.Changeset errors into a structured, i18n-ready
shape:
%{field => [%{code: atom, params: map, message: String.t}]}The shape is semver-stable from 0.12 onward. Frontends (generated
LiveView forms and REST controllers) key on :code to render
localized UI; :message carries a rendered string for callers
that don't own localization.
Why structured
Phoenix's out-of-the-box error format (%{field => [msg]}) is
ergonomic but impossible to localize on the frontend — by the time
the string reaches the Svelte layer, the structure is gone. Teams
shipping beyond English have had to parse strings or re-validate
client-side.
Structured %{code, params, message} lets a frontend:
- Key its translations on
:code(stable across locales). - Interpolate
:params(%{count: 3}) into its own translation template. - Fall back to
:messagewhen no frontend translation exists.
The server still renders a :message — via the configured
translator or a built-in pass-through interpolator — so
monolingual apps need no extra setup.
Configuring a translator
Apps using Gettext plug their backend in:
config :caravela, :changeset_translator, MyAppWeb.GettextCaravela calls MyAppWeb.Gettext.dgettext("errors", template, params)
(and dngettext/5 for plural forms signalled via :count) — the
exact pattern Phoenix's own ErrorHelpers.translate_error/1 uses,
so existing locale files under priv/gettext/<locale>/LC_MESSAGES/ errors.po work unchanged.
Per-call override:
Caravela.ChangesetTranslator.translate(changeset, translator: OtherGettext)Custom translators
Any module that exports dgettext/3 and dngettext/5 works —
the contract matches Gettext backends. Pass translator: false
to skip translation entirely and only interpolate parameters.
Code extraction
:code is Ecto's :validation option when present, then
:constraint, then :invalid as a generic fallback. Ecto's
sub-kinds (kind: :min, kind: :greater_than) stay in :params
— consumers normalize to friendly codes (:too_short,
:too_large) at their own discretion.
Summary
Functions
Translate every error on a changeset to the structured shape.
Translate a single {msg, opts} pair. Exposed so callers with
their own traversal (e.g. generator templates that unwrap nested
changesets before reporting) can reuse the same extraction logic.
Types
Functions
@spec translate( Ecto.Changeset.t(), keyword() ) :: errors()
Translate every error on a changeset to the structured shape.
Options:
:translator— an atom module exportingdgettext/3anddngettext/5(the Gettext backend contract). Falls back to theconfig :caravela, :changeset_translator, _value, and then to the pass-through interpolator when neither is set. Passfalseto force the pass-through path (useful in tests).
@spec translate_error({String.t(), keyword()}, keyword()) :: error()
@spec translate_error( {String.t(), keyword()}, module() | false | nil ) :: error()
Translate a single {msg, opts} pair. Exposed so callers with
their own traversal (e.g. generator templates that unwrap nested
changesets before reporting) can reuse the same extraction logic.