# `PhoenixKitWeb.Components.MultilangForm`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.164/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_multilang_apply_lang`

Applies a debounced language change. Call from `handle_info/2` when
`{:__multilang_apply_lang__, lang_code}` is received.

# `handle_switch_language`

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

Returns a socket that **defers** applying `:current_lang` via a short
trailing debounce (150 ms). Rapid click-through (EN → JA → FR → DE)
keeps rescheduling the timer; only the last click actually updates
`:current_lang` and triggers a content re-render. Without this, every
intermediate event caused its own server render and the client
briefly flashed each language's content before landing on the final
one.

Nothing to do on the consumer's end — `mount_multilang/1` attaches a
`:handle_info` hook via `Phoenix.LiveView.attach_hook/4` that
intercepts the internal `{:__multilang_apply_lang__, lang}` message
and applies the language transparently. Calling code never sees the
message.

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`, `:switching_lang` (no-op
compat assign — kept because consumer templates pass it through to
the wrapper). Also attaches an internal `:handle_info` hook that
receives the debounced language-switch timer message.

# `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)
* `switching_lang` (`:boolean`) - accepted for backwards compatibility but no longer used — skeleton/fields visibility is client-side via `switch_lang_js/2`. Defaults to `false`.
* `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 an info tooltip next to the header. Default:
    true. The tooltip surface explains the primary-language /
    fallback semantics on hover; requires `show_header: true` to
    have an anchor element.

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

Toggles skeleton/fields visibility **client-side, instantly** (hides
`[data-translatable=fields]`, reveals `[data-translatable=skeletons]`)
and pushes `"switch_language"` to the server. The server side holds
the class state untouched — `handle_switch_language/2` just schedules
a 150 ms debounced timer that eventually updates `:current_lang`,
which changes the wrapper div ids and makes morphdom replace both
divs with their new-language versions (skeleton back to `hidden`,
fields visible with new content). Nothing on the server renders
`hidden` on the fields or removes `hidden` from the skeleton, so the
JS toggles never fight a server diff.

Returns a no-op when `lang_code == current_lang` — clicking the
already-active tab doesn't push and doesn't flash the skeleton.

# `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*
