# `PhoenixKitWeb.Components.MultilangForm`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.87/lib/phoenix_kit_web/components/multilang_form.ex#L1)

Shared multilang form components and helpers for PhoenixKit modules.

Provides the language tab switcher UI, skeleton loading placeholders,
translatable field components, and Elixir-side helpers for merging
multilang data in LiveView forms.

Designed for two main use cases:

1. **Whole-form translation** — wrap all translatable fields in a card with
   language tabs. The tab bar, skeleton placeholders, and field wrappers
   are handled automatically.

2. **Single-field translation** — drop a `<.translatable_field>` into any
   form to make one field translatable, with no tab UI required (the caller
   manages `current_lang` however they like).

## Usage in a LiveView

### Mount

    import PhoenixKitWeb.Components.MultilangForm

    def mount(params, session, socket) do
      # ... load your record and changeset ...
      {:ok, mount_multilang(socket)}
    end

### Events

    def handle_event("switch_language", %{"lang" => lang_code}, socket) do
      {:noreply, handle_switch_language(socket, lang_code)}
    end

    def handle_event("validate", %{"record" => params}, socket) do
      params = merge_translatable_params(params, socket, ["name", "description"],
        changeset: socket.assigns.changeset)
      changeset = MySchema.changeset(socket.assigns.record, params)
      {:noreply, assign(socket, :changeset, changeset)}
    end

### Template — whole-form translation

    <.multilang_tabs
      multilang_enabled={@multilang_enabled}
      language_tabs={@language_tabs}
      current_lang={@current_lang}
    />

    <.multilang_fields_wrapper
      multilang_enabled={@multilang_enabled}
      current_lang={@current_lang}
    >
      <.translatable_field
        field_name="name"
        form_prefix="catalogue"
        changeset={@changeset}
        schema_field={:name}
        multilang_enabled={@multilang_enabled}
        current_lang={@current_lang}
        primary_language={@primary_language}
        lang_data={@lang_data}
        label={gettext("Name")}
        required
      />
    </.multilang_fields_wrapper>

### Template — single-field translation (no tabs needed)

    <.translatable_field
      field_name="description"
      form_prefix="product"
      changeset={@changeset}
      schema_field={:description}
      multilang_enabled={@multilang_enabled}
      current_lang={@current_lang}
      primary_language={@primary_language}
      lang_data={@lang_data}
      label={gettext("Description")}
      type="textarea"
      rows={5}
    />

# `get_lang_data`

Gets the raw language data for the current language from a changeset.

Use this in templates to read override-only values for secondary language tabs.
Returns `%{}` when multilang is disabled.

# `handle_switch_language`

Handles the `"switch_language"` event. Call from `handle_event/3`.

Returns the updated socket with the new `current_lang`.
Ignores unknown language codes.

# `inject_db_field_into_data`

Injects a DB column value into the JSONB `data` field for multilang storage.

This handles the common pattern where a field exists both as a top-level DB
column (for queries/sorting) and inside the JSONB `data` (for translations).

On the primary language tab, reads from `params[field_name]` (the DB column input).
On secondary language tabs, reads from `params["lang_" <> field_name]` (the translation input).

The value is stored in the data map under `"_" <> field_name` (e.g. `"_title"`).

If no value is submitted (field not in form), preserves the existing value from
the changeset's JSONB data.

## Requirements

`assigns` must contain:
- `:multilang_enabled` — boolean
- `:primary_language` — the primary language code
- `:changeset` — an `Ecto.Changeset` with a `:data` field (JSONB)

## Examples

    # In handle_event("validate", ...)
    form_data =
      form_data
      |> inject_db_field_into_data("title", data_params, current_lang, socket.assigns)
      |> inject_db_field_into_data("slug", data_params, current_lang, socket.assigns)

# `merge_multilang_data`

Merges language-specific validated data into the full multilang JSONB structure.

Reads existing data from the changeset's `:data` field, then merges the new
`validated_data` for the given `lang_code`.

Handles three cases:
- Multilang enabled: uses `PhoenixKit.Utils.Multilang.put_language_data/3`
- Multilang disabled but data has multilang structure: preserves translations
- Flat data, no multilang: passes through as-is

The `changeset` must be an `Ecto.Changeset` with a `:data` field.
`assigns` must contain `:multilang_enabled`.

# `merge_translatable_params`

Merges translatable field params into the multilang `data` JSONB structure.

Takes the raw form params, the socket, and a list of translatable field names
(the DB column names, e.g. `["name", "description"]`). Returns updated params
with the `"data"` key set to the merged multilang structure.

On primary language tabs, reads from `params["name"]`.
On secondary language tabs, reads from `params["lang_name"]`.

Also preserves primary language values for non-translatable fields when on
secondary tabs via the `preserve_fields` option.

## Options

  * `:changeset` — the current changeset (required)
  * `:preserve_fields` — map of `%{"field_name" => :schema_field}` for fields
    that should keep their primary language DB column value on secondary tabs.
    Defaults to `%{}`.

# `mount_multilang`

Adds multilang assigns to the socket. Call from `mount/3`.

Adds: `:multilang_enabled`, `:primary_language`, `:current_lang`, `:language_tabs`,
`:show_multilang_tabs`

# `multilang_enabled?`

Returns true when the Languages module is enabled with 2+ languages.

# `multilang_fields_wrapper`

Renders skeleton loading placeholders and a content wrapper for translatable fields.

The skeleton is shown instantly on tab switch via JS, then hidden when LiveView
re-renders with the new language data.

Wrap your translatable form fields inside this component's inner block.

## Customizing skeletons

Use the `:skeleton` slot to provide custom skeleton markup that matches
your form layout. If omitted, a default two-field skeleton is rendered.

## Attributes

  * `multilang_enabled` — boolean
  * `current_lang` — current language code (used in element IDs for morphdom)
  * `skeleton_class` — CSS class for the skeleton container. Default: `"card-body pt-4"`
  * `fields_class` — CSS class for the fields container. Default: nil

## Example — default skeleton (card context)

    <.multilang_fields_wrapper multilang_enabled={@multilang_enabled} current_lang={@current_lang}>
      <%!-- translatable fields here --%>
    </.multilang_fields_wrapper>

## Example — custom skeleton and classes (non-card context)

    <.multilang_fields_wrapper
      multilang_enabled={@multilang_enabled}
      current_lang={@current_lang}
      skeleton_class="space-y-6"
      fields_class="space-y-6"
    >
      <:skeleton>
        <div class="grid grid-cols-2 gap-6">
          <div class="skeleton h-12 w-full"></div>
          <div class="skeleton h-12 w-full"></div>
        </div>
      </:skeleton>
      <%!-- translatable fields here --%>
    </.multilang_fields_wrapper>

## Attributes

* `multilang_enabled` (`:boolean`) (required)
* `current_lang` (`:string`) (required)
* `skeleton_class` (`:string`) - Defaults to `"card-body pt-4"`.
* `fields_class` (`:string`) - Defaults to `nil`.
## Slots

* `skeleton`
* `inner_block` (required)

# `multilang_tabs`

Renders the language tab bar with compact/full mode.

Shows a header with language icon, an info alert explaining the translation
workflow, and the shared `<.language_switcher>` in `:tabs` variant with flags,
names, and primary star indicator.

Display mode:
- `compact: nil` (default) — auto: full names when ≤ 5 languages, short codes when more
- `compact: true` — always short codes
- `compact: false` — always full names

Delegates to `PhoenixKitWeb.Components.LanguageSwitcher.language_switcher/1`
for the tab bar rendering.

## Attributes

  * `multilang_enabled` — boolean, whether multilang is active
  * `language_tabs` — list of tab maps from `PhoenixKit.Utils.Multilang.build_language_tabs/0`
  * `current_lang` — the currently selected language code
  * `compact` — force compact mode (short codes). Default: nil (auto)
  * `show_header` — show the "Content Language" header. Default: true
  * `show_info` — show the info alert. Default: true

## Attributes

* `multilang_enabled` (`:boolean`) (required)
* `language_tabs` (`:list`) (required)
* `current_lang` (`:string`) (required)
* `compact` (`:boolean`) - Defaults to `nil`.
* `show_header` (`:boolean`) - Defaults to `true`.
* `show_info` (`:boolean`) - Defaults to `true`.
* `class` (`:string`) - Defaults to `"card-body pb-0"`.

# `preserve_primary_fields`

Preserves primary-language DB field values when on a secondary language tab.

On secondary tabs, some fields (like title, slug) are absent from form params
because they're replaced by `lang_*` inputs. This function fills in the missing
values from the changeset so the DB columns keep their primary-language values.

`preserve_fields` is a map of `%{"field_name" => :schema_field}`.

No-ops when multilang is disabled or on the primary tab.

# `primary_tab?`

Returns true when on the primary language tab (or multilang is disabled).

# `refresh_multilang`

Refreshes multilang assigns after external changes (e.g. entity schema update).

Unlike `mount_multilang/1`, this preserves `current_lang` when it's still valid,
and resets it to the primary language if it was removed.

# `switch_lang_js`

Returns a `Phoenix.LiveView.JS` command that switches languages.

Pushes the `"switch_language"` event, hides current field content,
and shows skeleton placeholders for instant visual feedback.

Returns a no-op when `lang_code == current_lang` to prevent skeleton ghosts.

# `translatable_field`

Renders a translatable text input or textarea field.

On the primary language tab, renders a standard input reading from the changeset.
On secondary language tabs, renders with a language-specific name and uses the
primary language value as placeholder text.

Works both inside a `<.multilang_fields_wrapper>` (whole-form translation) and
standalone (single-field translation).

## Translation models

Supports two naming patterns for secondary language inputs:

**Default (JSONB data column)** — used by entity data records:
- Secondary name: `form_prefix[lang_field_name]`
- Lang data key: `"_field_name"`

**Settings translations** — used by entity definitions and other models
that store translations in a `settings["translations"]` map. Set
`secondary_name` and `lang_data_key` to override the defaults:

    <.translatable_field
      field_name="display_name"
      form_prefix="entities"
      secondary_name={"entities[translations][#{@current_lang}][display_name]"}
      lang_data_key="display_name"
      ...
    />

## Attributes

  * `field_name` — the DB column name (e.g., "name")
  * `form_prefix` — the form name prefix (e.g., "catalogue")
  * `changeset` — the Ecto changeset
  * `schema_field` — the schema field atom (e.g., `:name`)
  * `multilang_enabled` — boolean
  * `current_lang` — current language code
  * `primary_language` — primary language code
  * `lang_data` — raw language data map for the current language
  * `label` — field label text
  * `placeholder` — placeholder for primary language (optional)
  * `type` — "input" or "textarea". Default: "input"
  * `rows` — textarea rows (only for type="textarea"). Default: 3
  * `required` — marks the primary language field as required. Default: false
  * `disabled` — disables the field. Default: false
  * `class` — additional CSS class(es) for the input element. Default: nil
  * `pattern` — HTML pattern attribute for input validation. Default: nil
  * `title` — HTML title attribute (for pattern validation message). Default: nil
  * `hint` — hint text shown below the field. Default: nil
  * `secondary_hint` — hint text shown only on secondary language tabs. Default: nil
  * `secondary_name` — override the secondary tab input name. Default: `"form_prefix[lang_field_name]"`
  * `lang_data_key` — key to look up in `lang_data` for secondary value.
    Default: `"_field_name"`. Set to `"field_name"` for settings translations.

## Attributes

* `field_name` (`:string`) (required)
* `form_prefix` (`:string`) (required)
* `changeset` (`:any`) (required)
* `schema_field` (`:atom`) (required)
* `multilang_enabled` (`:boolean`) (required)
* `current_lang` (`:string`) (required)
* `primary_language` (`:string`) (required)
* `lang_data` (`:map`) (required)
* `label` (`:string`) (required)
* `placeholder` (`:string`) - Defaults to `nil`.
* `type` (`:string`) - Defaults to `"input"`.
* `rows` (`:integer`) - Defaults to `3`.
* `required` (`:boolean`) - Defaults to `false`.
* `disabled` (`:boolean`) - Defaults to `false`.
* `class` (`:string`) - Defaults to `nil`.
* `pattern` (`:string`) - Defaults to `nil`.
* `title` (`:string`) - Defaults to `nil`.
* `hint` (`:string`) - Defaults to `nil`.
* `secondary_hint` (`:string`) - Defaults to `nil`.
* `secondary_name` (`:string`) - Defaults to `nil`.
* `lang_data_key` (`:string`) - Defaults to `nil`.
## Slots

* `label_extra`

---

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