# `PhoenixKit.Modules.Storage.FileDetails`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.32.1/lib/modules/storage/schemas/file_details.ex#L1)

A media file's translatable details — title, alt text and description —
**in one language**.

Not a table. The text lives on the file row's `data` column (V199), and
this module is the one read and write path for it:

    %{
      "en-US" => %{"title" => "Harbour", "alt" => "Boats in a harbour"},
      "et" => %{"title" => "Sadam"}
    }

Every language holds its own text, independently. **No language is marked
as the primary one in the data**, so a site that changes its primary
language has nothing to convert: which text stands in for a missing
translation is decided when it is read.

This is deliberately not the `PhoenixKit.Utils.Multilang` structure, which
stores every other language as a diff against an embedded primary. That
pays off for a record with dozens of fields; for three it buys nothing and
ties the stored text to a setting that can change.

## Reading

    FileDetails.translated_alt(file, "et")      # "" when there is none
    FileDetails.translated_title(file, "et")    # nil when there is none
    FileDetails.for_locale(file, "et")          # all three

Each field resolves on its own: the language asked for (or a dialect of
it) → the site's current primary language → any other language. The alt
text never falls back to the file name: a file name read aloud helps
nobody, and `alt=""` correctly marks the image as undescribed.

The site's primary language is read from `PhoenixKit.Utils.Multilang`; a
caller resolving many files passes it once as `primary:`.

## Files from before V199

They hold a title and description in `metadata`, in no recorded language.
While a file's `data` is empty that text is its primary-language text. The
first save moves it into `data`; from then on `data` is the truth, and
`metadata` only receives a copy of the primary-language text for the
readers that still look there (the PDF processor's fill-if-missing title,
the viewer's stored title).

## Writing

A save replaces one language's text and nothing else:

    Storage.update_file_details(file, %{"title" => "Sadam"}, lang: "et")

# `t`

```elixir
@type t() :: %PhoenixKit.Modules.Storage.FileDetails{
  alt: String.t() | nil,
  description: String.t() | nil,
  title: String.t() | nil
}
```

# `changeset`

```elixir
@spec changeset(t(), map()) :: Ecto.Changeset.t()
```

Changeset for one language's text. `attrs` holds `"title"`, `"alt"` and
`"description"`; a key that is absent keeps its current value.

# `content_language`

```elixir
@spec content_language(String.t() | nil, keyword()) :: String.t()
```

The content language a page shown in `locale` edits and reads: the enabled
language that `locale` names — so a URL's bare `"et"` and the hook's
`"et-EE"` store under the one code the site has enabled. A locale that is
no enabled language (the Gettext default on a site that never enabled it)
is the primary language: text must not pile up under a code no page is
ever shown in.

Options: `:languages` — the enabled language codes, `:primary`.

# `fields`

```elixir
@spec fields() :: [String.t()]
```

The translatable field names.

# `file_attrs`

```elixir
@spec file_attrs(
  PhoenixKit.Modules.Storage.File.t(),
  t(),
  String.t() | nil,
  keyword()
) :: %{
  metadata: map(),
  data: map()
}
```

The `File.details_changeset/2` attrs that store `details` as `file`'s text
in `lang`.

Only that language's entry in `data` changes (an entry left with no text
is removed). A file from before V199 first gets its `metadata` text moved
into `data` under the primary language. When `lang` is the primary
language, `metadata` receives a copy — a cleared field as `""`, not
removed: the PDF processor fills a *missing* `"title"` from the document,
and a title the user cleared must stay cleared.

# `for_locale`

```elixir
@spec for_locale(PhoenixKit.Modules.Storage.File.t(), String.t() | nil, keyword()) ::
  %{
    title: String.t() | nil,
    alt: String.t(),
    description: String.t() | nil
  }
```

All three details of `file` in `locale`.

# `from_file`

```elixir
@spec from_file(PhoenixKit.Modules.Storage.File.t(), String.t() | nil, keyword()) ::
  t()
```

The text `file` holds in `lang` itself — no fallback to another language,
so an editor shows what that language really has. "Itself" includes the
same language stored at another precision (`"en"` for `"en-US"`), which a
save replaces; it does not include a sibling dialect (`"en-GB"`), which is
another language — an editor opened on it would save a copy of it.

Options: `:primary` — the site's primary language (default: the current
one).

# `language_name`

```elixir
@spec language_name(String.t()) :: String.t() | nil
```

The display name of content language `lang` — or `nil` on a site with a
single language, where there is nothing to tell the editor.

# `translated`

```elixir
@spec translated(
  PhoenixKit.Modules.Storage.File.t(),
  String.t(),
  String.t() | nil,
  keyword()
) ::
  String.t() | nil
```

One detail of `file` in `locale`: that language's text, else the primary
language's, else any language's, else `nil`. A `nil` locale reads the
primary language.

Options: `:primary` — the site's primary language (default: the current
one).

# `translated_alt`

```elixir
@spec translated_alt(PhoenixKit.Modules.Storage.File.t(), String.t() | nil, keyword()) ::
  String.t()
```

The alt text in `locale`, ready for an `alt` attribute: `""` when the file
has none. Never the file name.

# `translated_description`

```elixir
@spec translated_description(
  PhoenixKit.Modules.Storage.File.t(),
  String.t() | nil,
  keyword()
) ::
  String.t() | nil
```

The description in `locale`, or `nil`.

# `translated_title`

```elixir
@spec translated_title(
  PhoenixKit.Modules.Storage.File.t(),
  String.t() | nil,
  keyword()
) ::
  String.t() | nil
```

The title in `locale`, or `nil`.

---

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