Flicker.Facet.Format (Flicker v0.1.0)

Copy Markdown View Source

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

join(parts, opts \\ [])

@spec join([String.t()], keyword()) :: String.t()

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(["@"])
"@"

localized?()

@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.

range_label(facet, range, opts \\ [])

@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"

value_label(facet, value, opts \\ [])

@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.