Display formatting for facet values (Spec 018) —
the one module allowed to call Localize.*.
Token text is locale-invariant and localisation is a display layer
(ADR-013),
so every value that reaches a screen passes through here: a range pill's
interval, a number's grouping, a duration's units, a multi-value list's
conjunction. Concentrating that in one module means the with-localize and
without-localize implementations sit side by side per concern, rather than
a Code.ensure_loaded?/1 at every call site.
localize is an optional dependency. Without it every function here falls
back to ISO 8601 dates, plain digits, and English joins — correct, just not
localised. With it, CLDR supplies interval formats, number grouping,
plural-aware durations, and list conjunctions for over 700 locales.
Locale
Every function takes :locale in opts rather than reading
Localize.get_locale/0 at the point of formatting. Localize's locale is
process-scoped, and a LiveView component may format inside an async assign
or a different process than the one that mounted — so the locale travels
with the call. nil (the default) means "whatever the process has", which
is the right behaviour for a plain synchronous render.
Summary
Functions
Joins parts into a localised conjunction list — "A", "A and B",
"A, B, and C".
Whether CLDR formatting is available — true when the optional localize
dependency is present.
A range's display string — a preset's own label when the range came from one, otherwise the interval.
A facet value's display string.
Functions
Joins parts into a localised conjunction list — "A", "A and B",
"A, B, and C".
Public because it isn't only facet values that need conjoining: Spec 024's
trigger hint lists the configured trigger characters, and
Flicker.Messages.English must not call Localize.* itself — this module is
the only one allowed to (ADR-013).
Examples
iex> Flicker.Facet.Format.join(["@", "#"])
"@ and #"
iex> Flicker.Facet.Format.join(["@"])
"@"
@spec localized?() :: boolean()
Whether CLDR formatting is available — true when the optional localize
dependency is present.
Exposed so tests can assert which path they exercised rather than guessing from the output.
@spec range_label(Flicker.Facet.t(), Flicker.Facet.Range.t(), keyword()) :: String.t()
A range's display string — a preset's own label when the range came from one, otherwise the interval.
A preset renders as what the user chose ("Last 30 days"), not as the dates it
resolved to, which is the whole reason Flicker.Facet.Range carries
:preset.
Examples
iex> facet = Flicker.Facet.new(key: :created, type: :date_range)
...>
...> range = %Flicker.Facet.Range{
...> from: ~D[2026-06-30],
...> to: ~D[2026-07-29],
...> preset: :last_30_days
...> }
...>
...> Flicker.Facet.Format.range_label(facet, range)
"Last 30 days"
iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
...> Flicker.Facet.Format.range_label(facet, %Flicker.Facet.Range{from: 10, to: 50})
"10 – 50"
iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
...> Flicker.Facet.Format.range_label(facet, %Flicker.Facet.Range{from: 10, to: nil})
"10 or more"
iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
...> Flicker.Facet.Format.range_label(facet, %Flicker.Facet.Range{from: nil, to: 50})
"up to 50"
@spec value_label(Flicker.Facet.t(), term(), keyword()) :: String.t()
A facet value's display string.
Dispatches on the shape of the value rather than the facet's type, so a hand-built facet with an unexpected value still renders something sensible instead of raising.
Examples
iex> facet = Flicker.Facet.new(key: :status, type: :enum, value_labels: %{active: "Active"})
...> Flicker.Facet.Format.value_label(facet, :active)
"Active"
iex> facet = Flicker.Facet.new(key: :length, type: :duration)
...> label = Flicker.Facet.Format.value_label(facet, 9000)
...> label in ["2h30m", "2 hours, 30 minutes"]
true
iex> facet =
...> Flicker.Facet.new(
...> key: :status,
...> type: :enum,
...> multiple?: true,
...> value_labels: %{a: "A", b: "B"}
...> )
...>
...> Flicker.Facet.Format.value_label(facet, [:a, :b])
"A and B"The duration example above admits two answers on purpose: 2h30m is the
fallback, 2 hours, 30 minutes is CLDR's. Which one you get depends on
whether the optional localize dependency is present, and a doctest can't
branch on that.