# `Flicker.Facet.Format`
[🔗](https://github.com/team-alembic/flicker/blob/v0.1.0/lib/flicker/facet/format.ex#L1)

Display formatting for facet values ([Spec 018](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-018-rich-facet-types.md)) —
the one module allowed to call `Localize.*`.

Token text is locale-invariant and localisation is a display layer
([ADR-013](https://github.com/team-alembic/flicker/blob/main/docs/adrs/adr-013-canonical-tokens-localised-display.md)),
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.

# `join`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
