PhoenixKit.Utils.RecipientLocale (phoenix_kit v2.55.1)

Copy Markdown View Source

Resolves which locale a message should be rendered in for a given recipient.

Outbound messages (email, and the external notification channels) render on a background or request process that has no meaningful Gettext locale of its own — the sender's locale is not the recipient's. This module is the single answer to "whose language is this message in".

Where the preference lives

A user's dialect preference is stored full ("en-GB", "pt-BR") in custom_fields["preferred_locale"], written by PhoenixKit.Users.Auth.update_user_locale_preference/2 from the language switcher. Absent means the user never chose one.

Two consumers, two shapes

  • for_rendering/1 — for template lookup. Returns the full dialect and never nil, falling back to the site's content language and then "en". Template resolution does its own dialect→base narrowing ("en-GB" is tried before "en"), so handing it the dialect strictly beats pre-truncating.
  • base/1 — for Gettext, which keys on base codes and treats nil as "leave the current locale alone".

Installing it

in_locale/2 runs a function with a locale installed on the process, which is how a Gettext-backed default reaches the right language: these render on a background worker or on behalf of another user, so the locale arrives as a value rather than being ambient. nil means "leave the current locale alone" — what a screen rendering for its own viewer wants. It installs the locale for the core backend and for every feature module's backend alike, as the web does (PhoenixKitWeb.Users.Auth.put_gettext_locale/2).

Failure

for_rendering/1 is total. The site default is read through PhoenixKit.Settings, which can raise on an unowned checkout or exit on a dead pool; both are caught and answered with "en". An unreachable database must degrade a message to English, never fail the send — this is the path PhoenixKit.Users.LoginAlerts calls during sign-in.

Summary

Functions

The recipient's preference as a base language code, or nil.

The locale to render a template in for recipient. Never nil.

The PhoenixKitWeb.Gettext locale that translates locale.

Runs fun with locale installed on the process, restoring it afterwards.

The recipient's stored dialect preference, or nil when they have none.

Functions

base(recipient)

@spec base(term()) :: String.t() | nil

The recipient's preference as a base language code, or nil.

For Gettext, which keys on base codes; nil means "no preference, leave the current locale alone".

iex> alias PhoenixKit.Utils.RecipientLocale
iex> RecipientLocale.base(%{custom_fields: %{"preferred_locale" => "pt-BR"}})
"pt"

for_rendering(recipient)

@spec for_rendering(term()) :: String.t()

The locale to render a template in for recipient. Never nil.

Preference → the site's content language → "en".

iex> alias PhoenixKit.Utils.RecipientLocale
iex> RecipientLocale.for_rendering(%{custom_fields: %{"preferred_locale" => "uk"}})
"uk"

gettext_locale(locale)

@spec gettext_locale(String.t()) :: String.t()

The PhoenixKitWeb.Gettext locale that translates locale.

Preferences and Languages codes are dialects ("es-ES", "pt-BR"), but the backend looks a locale up exactly and its catalogues are named for base languages (es), or, following Gettext's convention, pt_BR for a dialect. So: locale itself when the catalogue has it, else its Gettext spelling (pt-BR → pt_BR), else its base language — split on - or _. Installing "es-ES" as it was matched no catalogue, and every default came out in English.

Shared by the web (PhoenixKitWeb.Users.Auth.put_gettext_locale/2) and by in_locale/2, so the two cannot pick different translations for one language. (base/1 still hands in_locale/2 a pre-truncated base code, so the notification workers read the base catalogue even where a dialect one exists; core ships none.)

iex> PhoenixKit.Utils.RecipientLocale.gettext_locale("es-ES")
"es"
iex> PhoenixKit.Utils.RecipientLocale.gettext_locale("ru")
"ru"

in_locale(locale, fun)

@spec in_locale(String.t() | nil, (-> result)) :: result when result: term()

Runs fun with locale installed on the process, restoring it afterwards.

A nil locale runs fun untouched, so a caller with no recipient preference keeps whatever locale is already in force.

Two locales are installed, both restored afterwards (also when fun raises):

  • PhoenixKitWeb.Gettext gets gettext_locale/1, so a dialect the core catalogue does not have reads its base language's translation.
  • The process-global Gettext locale gets the base language ("es" for "es-ES"). This is the one a feature module's own backend reads (PhoenixKitBilling.Gettext in a billing default, any backend called while PhoenixKit.Email.Content resolves a default or a notification renders): a backend with no locale of its own falls back to the global one. Without it a module's text came out in the sender's language — the global locale of the worker or of the admin's LiveView — not the recipient's.

The global value is the base code because that is how module catalogues are named (es, et, ru), and Gettext matches a locale exactly: "es-ES" or "es_ES" would match none of them. A module that ships a dialect catalogue (pt_BR) is read in its base language here; its backend, unlike the core one, is not known to this module.

A backend that already has a locale of its own on this process (Gettext.put_locale(backend, _)) keeps it — that is an explicit choice this function does not override.

iex> alias PhoenixKit.Utils.RecipientLocale
iex> RecipientLocale.in_locale(nil, fn -> :ran end)
:ran
iex> RecipientLocale.in_locale("es-ES", fn -> Gettext.get_locale(PhoenixKitWeb.Gettext) end)
"es"
iex> RecipientLocale.in_locale("es-ES", fn -> Gettext.get_locale() end)
"es"

preferred(arg1)

@spec preferred(term()) :: String.t() | nil

The recipient's stored dialect preference, or nil when they have none.

Accepts anything: a %User{}, a plain map, or a bare email string (used by the magic-link registration path, where no account exists yet).

iex> alias PhoenixKit.Utils.RecipientLocale
iex> RecipientLocale.preferred(%{custom_fields: %{"preferred_locale" => "en-GB"}})
"en-GB"
iex> RecipientLocale.preferred("someone@example.com")
nil