Localize

View Source

Introduction

Visitors switch locale with a Corex <.language_switch> (a <.select redirect>). Choosing a language keeps the same logical page (/en/accordion/fr/accordion) by rewriting the locale segment of the current path.

Localized apps also set lang and dir on <html> from MyAppWeb.Locale so screen readers, font shaping, and bidi text work. Static Tableau sites use permalink-based locales instead; see Tableau Localize.

Install first

Wire localize_web, Gettext locales, router plugs / localize do, MyAppWeb.Locale, and (for LiveViews) Hooks.Layout before you drop this UI into a layout:

Already wired?

PieceExpect
Depslocalize_web (and Gettext / CLDR data for supported locales)
RouterLocalize.Plug.PutLocale / PutSession, routes inside localize do / /:locale
Locale moduleMyAppWeb.Locale with locales/0, current/0, label/1, swap_path/2, lang/0, dir/0
LiveViewon_mount MyAppWeb.Hooks.Layout (or equivalent) so :current_path tracks navigation
HookSelect registered in assets/js/app.js

Language switcher

The switcher needs the full request path (including the locale segment) so Locale.swap_path/2 can rewrite the first segment. mix corex.new --lang adds a :current_path assign on Layouts.app and typically renders <.language_switch> in the footer.

attr :current_path, :string, default: "/", doc: "request path for locale switching"

def language_switch(assigns) do
  current_path = assigns.current_path || "/"
  current = MyAppWeb.Locale.current()

  items =
    for locale <- MyAppWeb.Locale.locales(), into: [] do
      dest = MyAppWeb.Locale.swap_path(current_path, locale)

      Corex.List.Item.new(%{
        value: dest,
        label: MyAppWeb.Locale.label(locale),
        to: dest
      })
    end

  selected = [MyAppWeb.Locale.swap_path(current_path, current)]

  assigns =
    assigns
    |> assign(:items, items)
    |> assign(:value, selected)

  ~H"""
  <.select
    id="corex-language-switch"
    class="select ui-size-sm max-w-6xs"
    items={@items}
    value={@value}
    redirect
    positioning={%Corex.Positioning{same_width: true}}
  >
    <:label class="sr-only">Language</:label>
    <:item :let={item}>{item.label}</:item>
    <:trigger>
      <.heroicon name="hero-language" />
    </:trigger>
    <:item_indicator>
      <.heroicon name="hero-check" />
    </:item_indicator>
  </.select>
  """
end

redirect navigates to each item’s to path. Items use Corex.List.Item so value, label, and destination stay aligned.

Layout placement

Controllers / HEEx: pass the connection path:

<Layouts.app flash={@flash} current_path={@conn.request_path}>
  <h1>{gettext("Home")}</h1>
</Layouts.app>

Add mode, theme, and other assigns when you use those features.

LiveViews: Hooks.Layout (from --lang) should assign :current_path on mount and keep it updated on live_patch / live_navigate. Then render the switcher in the shell:

<.language_switch current_path={@current_path} />

Root layout should set lang and dir from MyAppWeb.Locale (install wiring). With Corex Design and mode/theme, also set data-theme / data-mode on <html>; see Dark mode and Theming.

Troubleshooting

SymptomCheck
Switcher missing or emptyLocale.locales/0 matches supported locales; Select hook registered
Wrong page after switchcurrent_path is the full request path (including locale segment)
Stale path after LiveView navigateHooks.Layout (or your handle_params) updates :current_path
Labels look like EN / FRCLDR data downloaded for those locales (mix localize.download_locales)
dir wrong for RTLLocale.dir/0 and root dir={MyAppWeb.Locale.dir()}