# `PhoenixKit.Modules.Languages`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.62.1/lib/modules/languages/languages.ex#L1)

Languages management for PhoenixKit - complete language configuration in a single module.

Languages is **core and always on**; it is not a toggleable feature module (it
has no card on the Modules page and is not in `PhoenixKit.ModuleRegistry`). What an
admin switches on or off, at **Admin → Settings → Languages**, is *multi-language
support for the site*: `enabled?/0`. Off, the site serves its configured fallback locale only
(`enabled_locale_codes/0`), `get_languages/0` and `get_enabled_languages/0` are
empty, and no menu or switcher offers a language. On, the configured languages are
routed and offered. The `languages` permission key is a core section key, so the
settings page stays reachable while the switch is off.

It handles language configuration, settings, and language data through JSON settings.

## Language Structure

Each language has the following structure:
- `code`: Language code (e.g., "en-US", "es-ES", "fr-FR")
- `name`: Full language name (e.g., "English (United States)", "Spanish (Spain)", "French (France)")
- `is_default`: Boolean indicating if this is the default language
- `is_enabled`: Boolean indicating if this language is active

## Core Functions

### Languages Management
- `enabled?/0` - Whether multi-language support is switched on for the site
- `enable_system/0` - Switch it on, with default English
- `disable_system/0` - Switch it off (the configured languages are kept)
- `get_config/0` - Get complete configuration

### Language Management
- `get_languages/0` - Get all configured languages
- `get_enabled_languages/0` - Get only enabled languages
- `get_default_language/0` - Get the default language
- `get_language/1` - Get a specific language by code
- `get_language_codes/0` - Get list of all language codes
- `get_enabled_language_codes/0` - Get list of enabled language codes
- `valid_language?/1` - Check if a language code exists
- `language_enabled?/1` - Check if a language is enabled
- `add_language/1` - Add a new language to the system
- `update_language/2` - Update an existing language
- `remove_language/1` - Remove a language from the system
- `set_default_language/1` - Set a new default language
- `enable_language/1` - Enable a specific language
- `disable_language/1` - Disable a specific language

## Usage Examples

    # Check if languages are enabled
    if PhoenixKit.Modules.Languages.enabled?() do
      # Languages are active
    end

    # Enable languages (creates default English language)
    {:ok, config} = PhoenixKit.Modules.Languages.enable_system()

    # Add a new language
    {:ok, config} = PhoenixKit.Modules.Languages.add_language("es")

    # Get all languages
    languages = PhoenixKit.Modules.Languages.get_languages()
    # => [%Language{code: "en-US", name: "English (United States)", is_default: true, is_enabled: true}, ...]

    # Get only enabled languages (most common use case)
    enabled_languages = PhoenixKit.Modules.Languages.get_enabled_languages()
    # => [%Language{code: "en-US", name: "English (United States)", ...}, ...]

    # Get a specific language by code
    spanish = PhoenixKit.Modules.Languages.get_language("es-ES")
    # => %{code: "es-ES", name: "Spanish (Spain)", is_enabled: true}

    # Get just the language codes
    codes = PhoenixKit.Modules.Languages.get_enabled_language_codes()
    # => ["en-US", "es-ES", "fr-FR"]

    # Check if a language is valid and enabled
    if PhoenixKit.Modules.Languages.language_enabled?("es-ES") do
      # Use Spanish language
    end

## JSON Storage Format

Languages are stored in the `languages_config` setting as JSON.
The array order determines the display order:

    {
      "languages": [
        {
          "code": "en-US",
          "name": "English (United States)",
          "is_default": true,
          "is_enabled": true
        },
        {
          "code": "es-ES",
          "name": "Spanish (Spain)",
          "is_default": false,
          "is_enabled": true
        }
      ]
    }

# `add_language`

Adds a predefined language to the module by language code.

Takes a language code and adds the corresponding predefined language
to the module configuration. Only languages from the predefined list
can be added.

## Examples

    iex> PhoenixKit.Modules.Languages.add_language("es-ES")
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.add_language("en-US")  # if already exists
    {:error, "Language already exists"}

    iex> PhoenixKit.Modules.Languages.add_language("invalid")
    {:error, "Language not found in available languages"}

# `configured_primary_language_code`

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

The primary language an admin (or the install) actually configured, or nil
when none is: `primary_language_code/0` without its last fallback, the
runtime `default_locale`.

For a caller that treats "configured" and "assumed" differently — the content
language of a site with nothing configured is the BASE of `default_locale`
(`"en"`, as it always was), where a configured `en-US` stays `en-US`.

# `default_language_no_prefix?`

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

Returns true when public + admin URLs should omit the locale segment
for the primary language.

Site-wide setting (key `default_language_no_prefix`). When `true`,
every URL generator that honors it — `Routes.path/1`,
`Routes.admin_path/2`, the sitemap, publishing's HTML builders, the
publishing canonical-redirect controller, and the publishing
`RouterDispatch` rewriter — emits the primary language as the
prefixless shape (`/admin/users`, `/blog/post`). When `false`, all
generators emit the prefixed shape (`/en/admin/users`,
`/en/blog/post`).

Default: `false` (matches the historical publishing default and
keeps existing installs' indexed URLs stable on upgrade). The
setting can be migrated from the legacy
`publishing_default_language_no_prefix` key — see `migrate_legacy/0`.

# `default_language_no_prefix_key`

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

Returns the storage key used for the no-prefix setting. Exposed for
the legacy-key migration in `PhoenixKit.Migration`; callers should
prefer `default_language_no_prefix?/0`.

# `disable_language`

Disables a specific language.

Cannot disable the default language.

## Examples

    iex> PhoenixKit.Modules.Languages.disable_language("es-ES")
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.disable_language("en-US")  # if it's default
    {:error, "Cannot disable default language"}

# `disable_system`

Switches multi-language support off. The configured languages are kept, so
switching it on again restores them.

Returns `{:ok, setting}` on success, `{:error, changeset}` on failure.

## Examples

    iex> PhoenixKit.Modules.Languages.disable_system()
    {:ok, %Setting{}}

# `enable_language`

Enables a specific language.

## Examples

    iex> PhoenixKit.Modules.Languages.enable_language("es-ES")
    {:ok, updated_config}

# `enable_system`

Switches multi-language support on and creates the default configuration.

Creates the initial configuration with the site's own language as the default
(the language `config :phoenix_kit, default_locale` names, English unless set).
If a previous configuration exists, it is restored instead of reset — a
primary language chosen while the switch was off included. The first time
the configuration is created it also turns `default_language_no_prefix` on
(unless that was saved before), so a site that served clean URLs keeps them.
Updates both the enabled flag and the JSON configuration.

Returns `{:ok, config}` on success, `{:error, reason}` on failure.

## Examples

    iex> PhoenixKit.Modules.Languages.enable_system()
    {:ok, %{"languages" => [%{"code" => "en-US", ...}]}}

# `enabled?`

Whether multi-language support is switched on for the site (the switch on
Admin → Settings → Languages).

Returns true if it is on, false otherwise.

## Examples

    iex> PhoenixKit.Modules.Languages.enabled?()
    false

# `enabled_locale_codes`

Gets enabled language codes for locale-based routing.

Returns a list of enabled language codes that can be used in URL routing.
Falls back to the primary language (`primary_language_code/0`) when
multi-language support is off.

## Examples

    iex> PhoenixKit.Modules.Languages.enabled_locale_codes()
    ["en-US", "es-ES", "fr-FR"]

    # When system is disabled:
    iex> PhoenixKit.Modules.Languages.enabled_locale_codes()
    ["en-US"]

# `get_available_language_by_code`

Gets a language by code with O(1) lookup from cached map.
Returns nil if not found.

# `get_available_languages`

Gets all available predefined languages.

Returns the complete list of languages that can be added to the system.
This list is used to populate dropdown selections.

## Examples

    iex> PhoenixKit.Modules.Languages.get_available_languages()
    [%{code: "en-US", name: "English (United States)", native: "English (US)", flag: "🇺🇸"}, ...]

# `get_available_languages_for_selection`

Gets available languages for selection (excludes already added languages).

Returns languages that can be added to the system, filtered to exclude
languages that have already been configured.

## Examples

    iex> PhoenixKit.Modules.Languages.get_available_languages_for_selection()
    [%{code: "es-ES", name: "Spanish (Spain)", native: "Español (España)", flag: "🇪🇸"}, ...]

# `get_config`

Gets the complete language module configuration.

Returns a map with module status and language configuration.

## Examples

    iex> PhoenixKit.Modules.Languages.get_config()
    %{
      enabled: true,
      languages: [%{"code" => "en-US", "name" => "English (United States)", ...}],
      language_count: 1,
      enabled_count: 1,
      default_language: %{"code" => "en-US", "name" => "English (United States)", ...}
    }

# `get_default_language`

Gets the default language.

Returns the language map marked as default, or nil if none found.

## Examples

    iex> PhoenixKit.Modules.Languages.get_default_language()
    %{"code" => "en-US", "name" => "English (United States)", "is_default" => true, ...}

    # When system is disabled:
    iex> PhoenixKit.Modules.Languages.get_default_language()
    nil

# `get_default_language_codes`

Gets the default popular languages for admin panel display.

Returns a list of the most commonly used language codes that should
be available in the admin panel language selector.

## Examples

    iex> PhoenixKit.Modules.Languages.get_default_language_codes()
    ["en-US", "es-ES", "fr-FR", "de-DE", "pt-BR", "it", "nl", "ru", "ja", "ko", "zh", "ar", "et"]

# `get_display_languages`

Gets the legacy language preview list.

Returns the configured languages if the module is enabled.
Otherwise, returns the hardcoded preview defaults. The admin Languages page
now uses `get_languages/0`; this helper retains its behavior for existing callers.

⚠️ **Not a list to offer a visitor.** With the module off, the defaults are
languages the site does not serve (`enabled_locale_codes/0` is just the default
locale), so a switcher built from them links to routes that do not exist. A menu
or switcher takes `get_enabled_languages/0`, which is empty while the module is off.

## Examples

    # When enabled (any number of languages)
    iex> PhoenixKit.Modules.Languages.get_display_languages()
    [%{"code" => "en-US", "name" => "English (United States)", ...}]

    # When disabled
    iex> PhoenixKit.Modules.Languages.get_display_languages()
    [%{"code" => "en-US", ...}, %{"code" => "es-ES", ...}, ...]  # Preview defaults

# `get_enabled_language_codes`

Gets a list of enabled language codes, sorted by position.

Returns a list of enabled language code strings.

## Examples

    iex> PhoenixKit.Modules.Languages.get_enabled_language_codes()
    ["en-US", "es-ES"]

# `get_enabled_languages`

Gets only enabled languages, sorted by position.

Returns a list of enabled language maps.

## Examples

    iex> PhoenixKit.Modules.Languages.get_enabled_languages()
    [%{"code" => "en-US", "name" => "English (United States)", ...}]

# `get_enabled_languages_by_continent`

Gets enabled languages grouped by continent.

Filters `get_languages_grouped_by_continent/0` to only include languages
that are currently enabled. Languages may appear under multiple continents
(same as the admin settings page). Returns `[{continent, [language_maps]}]`.

## Examples

    iex> PhoenixKit.Modules.Languages.get_enabled_languages_by_continent()
    [{"Asia", [%{code: "ja", name: "Japanese", ...}]}, {"Europe", [%{code: "de-DE", ...}]}, ...]

# `get_language`

Gets a specific language by its code.

Returns the language map if found, or nil if not found.

## Examples

    iex> PhoenixKit.Modules.Languages.get_language("es-ES")
    %{"code" => "es-ES", "name" => "Spanish (Spain)", "is_enabled" => true}

    iex> PhoenixKit.Modules.Languages.get_language("invalid")
    nil

# `get_language_codes`

Gets a list of all language codes.

Returns a list of language code strings.

## Examples

    iex> PhoenixKit.Modules.Languages.get_language_codes()
    ["en-US", "es-ES", "fr-FR"]

# `get_languages`

Gets all configured languages from the JSON setting.

Returns a list of language maps, or empty list if not configured.

## Examples

    iex> PhoenixKit.Modules.Languages.get_languages()
    [%{"code" => "en-US", "name" => "English (United States)", "is_default" => true, ...}]

    # When system is disabled:
    iex> PhoenixKit.Modules.Languages.get_languages()
    []

# `get_languages_grouped_by_continent`

Gets all available languages grouped by continent, then by country.

Returns a list of {continent, countries} tuples where countries is a list of
{country, flag, languages} tuples. Sorted alphabetically by continent and country.
Only shows languages that are actually used in each country based on the
`language_locales` field from BeamLabCountries.

## Examples

    iex> PhoenixKit.Modules.Languages.get_languages_grouped_by_continent()
    [
      {"Africa", [{"South Africa", "🇿🇦", [%{code: "en-ZA", ...}]}, ...]},
      {"Asia", [{"China", "🇨🇳", [%{code: "zh-CN", ...}]}, ...]},
      {"Europe", [{"Germany", "🇩🇪", [%{code: "de-DE", ...}]}, ...]},
      ...
    ]

# `get_predefined_language`

Gets a predefined language by code.

Returns the language definition from the available languages list.

## Examples

    iex> PhoenixKit.Modules.Languages.get_predefined_language("es-ES")
    %{code: "es-ES", name: "Spanish (Spain)", native: "Español (España)", flag: "🇪🇸"}

    iex> PhoenixKit.Modules.Languages.get_predefined_language("invalid")
    nil

# `in_configured_mode?`

Checks if currently in configured mode (showing user-configured languages vs defaults).

Returns true if the module is enabled and has 2+ configured languages.
Returns false if showing the default top 10 languages.

## Examples

    iex> PhoenixKit.Modules.Languages.in_configured_mode?()
    true  # When enabled with 2+ languages

    iex> PhoenixKit.Modules.Languages.in_configured_mode?()
    false  # When disabled or only 1 language

# `language_enabled?`

Checks if a language is enabled.

Returns true if the language exists and is enabled, false otherwise.

## Examples

    iex> PhoenixKit.Modules.Languages.language_enabled?("es-ES")
    true

    iex> PhoenixKit.Modules.Languages.language_enabled?("disabled_lang")
    false

# `migrate_legacy`

```elixir
@spec migrate_legacy() :: :ok | {:ok, map()}
```

Backfills the `default_language_no_prefix` setting from the legacy
`publishing_default_language_no_prefix` key the first time this runs
on an install that predates the site-wide setting. Idempotent — once
the new key is set, subsequent calls are no-ops.

Called once at startup by `PhoenixKit.Supervisor`, next to
`normalize_language_settings/0`. (Languages is core, not a registered module,
so `PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0` no longer reaches it.)

# `move_language_down`

Moves a language down one position in the array.

Moves the language one index later in the languages array. Cannot move the last language down.

## Examples

    iex> PhoenixKit.Modules.Languages.move_language_down("en-US")
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.move_language_down("es-ES")  # if last in array
    {:error, "Language is already at the bottom"}

# `move_language_up`

Moves a language up one position in the array.

Moves the language one index earlier in the languages array. Cannot move the first language up.

## Examples

    iex> PhoenixKit.Modules.Languages.move_language_up("es-ES")
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.move_language_up("en-US")  # if first in array
    {:error, "Language is already at the top"}

# `normalize_language_settings`

Normalizes language settings by merging any languages from the legacy
`admin_languages` setting into the unified `languages_config`.

This is a one-time migration function that:
1. Reads the old `admin_languages` JSON array of codes
2. Ensures the Languages module is enabled if admin languages existed
3. Adds any admin-only languages to the unified config
4. Clears the old `admin_languages` setting to prevent re-processing

Idempotent — if `admin_languages` doesn't exist or is empty, this is a no-op.

## Examples

    iex> PhoenixKit.Modules.Languages.normalize_language_settings()
    :ok

# `prefixless_primary_safe?`

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

Boot- and mix-task-safe wrapper around `default_language_no_prefix?/0`.

URL-emission call sites (`PhoenixKit.Utils.Routes`,
`PhoenixKitWeb.Users.Auth`) need the setting before the application
is fully up — during `mix phoenix_kit.install`, during host-app
routing macros that execute at compile time, or when the Settings
table simply doesn't exist yet. This wrapper:

  * Returns `false` during mix-task context (detected via the same
    sentinel `Routes.path/1` uses) so admin URL emission stays
    sensible without DB.
  * Rescues any other exception (DB unreachable, schema mismatch,
    cache miss) to `false` — the safer default since prefixed URLs
    resolve under both setting states.

Use `default_language_no_prefix?/0` from runtime contexts where the
DB is guaranteed reachable. Use this from boot/middleware contexts.

# `primary_language_code`

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

The site's primary language code (`"uk"`, `"en-US"`), whether or not
multi-language support is on — the language of a single-language site.

Resolution, first hit wins:

  1. while the switch is on, `get_default_language/0` (the request-scoped
     override, then the configured default);
  2. the default language stored in `languages_config` — kept while the
     switch is off, and what `set_primary_language/1` writes;
  3. the runtime `config :phoenix_kit, default_locale` (default `"en-US"`).

Steps 1-2 are `configured_primary_language_code/0`. Unlike `get_default_language/0` it never returns nil, so callers do not
invent their own fallback (they used to hardcode `"en"`).

# `put_request_default_language`

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

Sets a request/process-scoped default-language override, for multi-domain
hosts where each domain has its own canonical language.

While set, `get_default_language/0` prefers the named language (when it is
enabled) over the site-wide `is_default` flag, which every URL generator
and content-language lookup in the workspace already delegates to.

Process-scoped: set it per conn (a Plug) and per LiveView socket (an
`on_mount` hook, wired via `config :phoenix_kit, :extra_live_session_on_mount`
— see `PhoenixKitWeb.Integration.build_live_surface/5`). Spawned tasks and
Oban jobs do NOT inherit it. Pass `nil` (or `""`, treated the same) to
clear/no-op.

⚠️ **Plug placement matters.** `PhoenixKitWeb.Users.Auth.validate_and_set_locale/2`
(the `:phoenix_kit_locale_validation` pipeline plug) already calls
`get_default_language/0` — via `Routes.get_default_admin_locale/0` — to pick
the Gettext locale AND decide locale-prefix redirects for the very first
(dead-render) response, and it runs BEFORE any `on_mount` hook, extra or
built-in, for that same request/process. A host conn-level plug that sets
this override must therefore run inside the host's OWN `:browser` pipeline
(i.e. before `phoenix_kit_routes()`'s `pipe_through`), not appended after
it — otherwise the dead-render renders in the site-wide default language
while the on_mount-set override only takes effect for content resolved
after mount, producing a visibly mixed-language initial page.

# `put_request_locale`

```elixir
@spec put_request_locale(String.t()) :: :ok
```

Records the full dialect the current request or LiveView navigation
resolved to (`"en-GB"`), on this process.

The Gettext locale cannot stand in for it: core ships catalogues under
bare base codes, so `"en-GB"` and `"en-US"` both put `"en"` there — and
anything that stores or reads per-language CONTENT by that value mixes two
co-enabled dialects up. Set next to the Gettext locale
(`PhoenixKitWeb.Users.Auth.put_gettext_locale/1`), so it is present
wherever that is, LiveComponents included.

# `remove_language`

Removes a language from the system.

Cannot remove the default language or the last remaining language.

## Examples

    iex> PhoenixKit.Modules.Languages.remove_language("es-ES")
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.remove_language("en-US")  # if it's default
    {:error, "Cannot remove default language"}

# `reorder_languages`

Reorders languages to match the given list of codes.

Languages not in `ordered_codes` keep their current position at the end.

# `request_default_language`

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

Returns the request/process-scoped default-language override, if any.

# `request_locale`

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

The dialect `put_request_locale/1` recorded on this process, if any.

# `set_default_language`

Sets a new default language.

Removes default status from all other languages and sets the specified language as default.

## Examples

    iex> PhoenixKit.Modules.Languages.set_default_language("es-ES")
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.set_default_language("nonexistent")
    {:error, "Language not found"}

# `set_default_language_no_prefix`

```elixir
@spec set_default_language_no_prefix(boolean()) ::
  {:ok, PhoenixKit.Settings.Setting.t()} | {:error, Ecto.Changeset.t()}
```

Persists the `default_language_no_prefix` setting. Returns
`{:ok, setting}` on success or `{:error, changeset}` on failure.

# `set_primary_language`

```elixir
@spec set_primary_language(String.t()) :: {:ok, map()} | {:error, term()}
```

Makes `code` the primary language, whether or not multi-language support is
on. Adds it to the configuration first when it is not there yet.

With nothing configured, the configuration becomes just this language — a
single-language site picking its language does not inherit the English
default that a first switch-on would otherwise start from.

# `single_language?`

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

Whether the site is single-language: multi-language support is off.

A single-language site emits no locale segment in any URL, and a locale
segment in a request is redirected away (see `PhoenixKit.Utils.Routes.path/2`
and `PhoenixKitWeb.Users.Auth.validate_and_set_locale/2`).

# `single_language_safe?`

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

Boot- and mix-task-safe `single_language?/0`, for URL emission.

Answers `false` (multilingual — today's URL shapes) during a mix task and
when the read raises or exits, the same failure policy as
`prefixless_primary_safe?/0`: a prefixed URL still routes on a site of
either kind.

# `update_language`

Updates an existing language in the system.

Takes a language code and map of attributes to update.

## Examples

    iex> PhoenixKit.Modules.Languages.update_language("es-ES", %{name: "Español"})
    {:ok, updated_config}

    iex> PhoenixKit.Modules.Languages.update_language("nonexistent", %{name: "Test"})
    {:error, "Language not found"}

# `valid_language?`

Checks if a language code is valid (exists in configuration).

Returns true if the language exists, false otherwise.

## Examples

    iex> PhoenixKit.Modules.Languages.valid_language?("es-ES")
    true

    iex> PhoenixKit.Modules.Languages.valid_language?("invalid")
    false

---

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