VerifiedTranslations (verified_translations v0.1.1)

Copy Markdown View Source

Compile-time verified translations for Phoenix.

Like Phoenix.VerifiedRoutes does for paths, the ~t sigil verifies at compile time that every message is translated to every configured locale, and fails compilation otherwise:

use VerifiedTranslations

~t"Welcome back"
~t"Hello #{name}"       # source "Hello %{name}", binding name
~t"Hello #{@user.name}" # source "Hello %{user_name}"

Each message lives in its own JSON file holding all locales, so parallel branches, worktrees and coding agents never conflict on translation files:

// priv/translations/default/hello-name-<128-bit hash>.json
{
  "msgid": "Hello %{name}",
  "fi": "Hei %{name}",
  "sv": "Hej %{name}"
}

Translations are inlined into the calling module at compile time, so there is no runtime lookup table and no Gettext backend to recompile. Editing a JSON file recompiles only the modules that use that message.

Configuration

config :verified_translations,
  locales: ["en", "fi", "sv"],
  source_locale: "en",            # language of the text in ~t
  path: "priv/translations",      # relative to the project root
  create_missing: true            # write stub files for new messages

use VerifiedTranslations accepts the same keys (plus :domain, default "default") to override the config per module.

Locale

The current locale is per process, like Gettext's:

VerifiedTranslations.put_locale("fi")
VerifiedTranslations.with_locale("sv", fn -> ~t"Hello #{name}" end)

Unknown or unset locales render the source text.

Summary

Functions

Returns the locale of the current process.

Configured locales, including the source locale.

Sets the locale of the current process.

Translates the text to the current locale, verified at compile time.

Translates a plural message, choosing the CLDR plural category of count for the current locale.

Runs fun with locale as the current locale, restoring the previous one.

Functions

get_locale(gettext_backend \\ nil)

@spec get_locale(module() | nil) :: String.t() | nil

Returns the locale of the current process.

Falls back to Gettext's locale (of gettext_backend if given) when put_locale/1 was not called, so apps that already set the Gettext locale keep working. Returns nil without Gettext.

known_locales()

@spec known_locales() :: [String.t()]

Configured locales, including the source locale.

put_locale(locale)

@spec put_locale(String.t()) :: :ok

Sets the locale of the current process.

When Gettext is available, also sets Gettext's global locale (and the configured :gettext_backend's locale) so gettext/ngettext calls stay in the same language.

sigil_t(text, modifiers)

(macro)

Translates the text to the current locale, verified at compile time.

Interpolated variables become bindings named after their access path: #{name} and #{@name} are %{name}, #{@user.name} is %{user_name}.

tn(count, forms)

(macro)

Translates a plural message, choosing the CLDR plural category of count for the current locale.

tn(length(files), one: "%{count} file", other: "%{count} files")
tn(@n, one: "#{@user.name} has one item", other: "#{@user.name} has %{count} items")

The forms must be exactly the source locale's CLDR categories (one and other for English). %{count} is the first argument; other values are interpolated like in ~t. The message file holds one object of forms per locale, verified to have exactly that locale's categories:

{"kind": "plural", "msgid": "%{count} files",
 "fi": {"one": "%{count} tiedosto", "other": "%{count} tiedostoa"}}

~t"#{count} file(s)"N (gettext_sigils compatible) is the same with one source text for every category.

Ordinals use CLDR's ordinal categories and are stored as "kind": "ordinal":

tn(place, ordinal: true, one: "%{count}st", two: "%{count}nd", few: "%{count}rd", other: "%{count}th")

with_locale(locale, fun)

@spec with_locale(String.t(), (-> result)) :: result when result: term()

Runs fun with locale as the current locale, restoring the previous one.