# `PhoenixKit.Utils.Multilang`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v3.5.0/lib/phoenix_kit/utils/multilang.ex#L1)

Multi-language data transformation helpers for entity data JSONB.

Multi-language support is driven by the Languages module globally.
When the Languages module is enabled and has more than one language,
all entities automatically support multilang data. There is no
per-entity toggle — languages are configured system-wide.

The `data` JSONB column stores a nested structure:

    %{
      "_primary_language" => "en-US",
      "en-US" => %{"_title" => "Acme", "name" => "Acme", "tagline" => "Quality products"},
      "es-ES" => %{"_title" => "Acme España", "name" => "Acme España"}
    }

The primary language always has complete data. Secondary languages
store only overrides — fields that differ from primary. Display
merges primary values as defaults with language-specific overrides.

The `_title` key stores the record title alongside custom fields,
unifying title translation with the same override-only storage pattern.
The `title` DB column remains a denormalized copy for queries/sorting.

# `build_language_tabs`

```elixir
@spec build_language_tabs() :: [map()]
```

Builds language tab data for the UI from the Languages module.
Returns a list of maps with code, name, flag, and is_primary fields.

`name` drops the country qualifier when only one dialect of its base
language is enabled ("German (Germany)" → "German"); enabling a sibling
dialect (en-US + en-GB) brings the qualifier back. Same rule as the
navigation dropdowns — see `PhoenixKitWeb.Components.Core.LanguageSwitcher.dedupe_names/1`.

# `build_text_data`

```elixir
@spec build_text_data(map()) :: map()
```

Builds multilang `data` from short text fields, as a host passes them at boot:

    build_text_data(%{"_name" => %{"en" => "Buyer", "de" => "Käufer"}})

A string goes under the primary language, a `%{lang => text}` map language by
language, and blanks are dropped. The primary entry is one that exists, because
every other language overrides it: the site's primary language or its base code,
else the first language given.

# `current_locale`

```elixir
@spec current_locale() :: String.t()
```

The language the page being rendered is in, for reading and storing
per-language content: the full dialect the request resolved to, else the
process's Gettext locale.

Read it from the process, not from an assign: a LiveComponent has no
`@current_locale` unless every host threads one through, and shares its
LiveView's process either way. Not the Gettext locale alone — that is
downgraded to a base code (`"en"` for both `"en-US"` and `"en-GB"`).

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Checks if multilang is enabled globally.
Returns true when the Languages module is enabled and has more than one language.

# `enabled_languages`

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

Gets the list of enabled language codes.
Returns at minimum the primary language.

# `flatten_to_primary`

```elixir
@spec flatten_to_primary(map() | nil) :: map()
```

Converts multilang data back to flat structure.
Returns primary language data.

# `get_language_data`

```elixir
@spec get_language_data(map() | nil, String.t()) :: map()
```

Extracts the data map for a specific language from a record's data.

For multilang data: returns merged data (primary as base + overrides).
For flat data: returns data as-is (backward compat).

# `get_primary_data`

```elixir
@spec get_primary_data(map() | nil) :: map()
```

Gets the primary language data from a record (for display in lists etc).

# `get_raw_language_data`

```elixir
@spec get_raw_language_data(map() | nil, String.t()) :: map()
```

Gets raw (non-merged) language-specific data for a language.
Used by the form UI to detect which fields are overridden vs inherited.

# `maybe_rekey_data`

```elixir
@spec maybe_rekey_data(map() | nil) :: map() | nil
```

Checks if data needs re-keying (embedded primary != global primary).
Returns re-keyed data if needed, original data otherwise.

# `migrate_to_multilang`

```elixir
@spec migrate_to_multilang(map() | nil, String.t()) :: map()
```

Converts existing flat data to multilang structure.

# `multilang_data?`

```elixir
@spec multilang_data?(map() | nil) :: boolean()
```

Checks if a data map uses the multilang structure.
Presence of `_primary_language` key indicates multilang.

# `primary_language`

```elixir
@spec primary_language() :: String.t()
```

Gets the primary (default) language code.
Returns the Languages module default, falling back to "en-US".

# `put_language_data`

```elixir
@spec put_language_data(map() | nil, String.t(), map()) :: map()
```

Merges language-specific form data into the full multilang JSONB.

For primary language: stores ALL fields.
For secondary language: stores only fields that differ from primary.

# `rekey_primary`

```elixir
@spec rekey_primary(map() | nil, String.t()) :: map()
```

Re-keys multilang data to a new primary language.

Updates `_primary_language` to the new primary and ensures the new
primary has complete data (fills missing fields from the old primary).
All secondary languages are recomputed: their overrides are recalculated
against the new promoted primary, and languages with zero overrides are removed.

Returns data unchanged if already using the given primary or not multilang.

# `text_field`

```elixir
@spec text_field(term(), String.t(), String.t() | nil) :: String.t() | nil
```

The text of a short translated `field` ("name", "description"), in `lang` or
the primary language, with blanks read as absent.

Reads the multilang form's `"_name"` first, then a bare `"name"` from data written
by hand. A language with no text inherits the primary language's. `nil` when
there is none, so the caller can fall back to something of its own.

# `valid_text_data?`

```elixir
@spec valid_text_data?(term()) :: boolean()
```

Whether `data` is a map `text_field/3` can read: flat text fields, or the multilang
shape with text fields under each language. For a castable `data` field, where a
string or a list would crash every reader.

---

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