Packages
phoenix_kit
2.41.2
2.41.4
2.41.3
2.41.2
2.41.1
2.41.0
2.40.1
2.40.0
2.39.0
2.38.1
2.38.0
2.37.5
2.37.4
2.37.3
2.37.2
2.37.1
2.37.0
2.36.1
2.36.0
2.35.0
2.34.0
2.33.0
2.32.1
2.32.0
2.31.1
2.31.0
2.30.0
2.29.1
2.29.0
2.28.2
2.28.1
2.28.0
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.0
2.23.3
2.23.2
2.23.1
2.23.0
2.22.24
2.22.23
2.22.22
2.22.21
2.22.20
2.22.19
2.22.18
2.22.17
2.22.16
2.22.15
2.22.14
2.22.13
2.22.12
2.22.11
2.22.10
2.22.9
2.22.8
2.22.7
2.22.6
2.22.5
2.22.4
2.22.3
2.22.2
2.22.1
2.22.0
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.0
2.19.0
2.18.1
2.18.0
2.17.0
2.16.0
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.19
2.13.18
2.13.17
2.13.16
2.13.15
2.13.13
2.13.12
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/gettext.ex
defmodule PhoenixKitWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
use Gettext, backend: PhoenixKitWeb.Gettext
# Simple translation
gettext("Here is the string to translate")
# Plural translation
ngettext("Here is the string to translate",
"Here are the strings to translate",
3)
# Domain-based translation
dgettext("errors", "Here is the error message to translate")
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
This backend does **not** `use Gettext.Backend`. That macro compiles each
message into a function clause, and ~2.7k messages × 7 translated locales
is superlinear in the Erlang compiler (a clean `mix compile --force` spent
~13s on this file even after `split_module_by: [:locale]`, tripping the
">10s" notice). Translations are parsed from `priv/gettext` at compile
time into a nested map, embedded as a compressed binary, and looked up
with `Map.get/2`. Callers still go through `Gettext.dgettext/3` and friends;
only the storage changes.
"""
@behaviour Gettext.Backend
require Logger
alias PhoenixKitWeb.Gettext.Compiler
@otp_app :phoenix_kit
# Host config wins over the defaults, exactly like `Gettext.Backend.__using__`.
# Every attribute below is derived from the MERGED opts -- reading a default
# directly would build the catalog from a configured `:priv` while reporting
# the unconfigured one to `mix gettext.extract`.
@opts [
otp_app: @otp_app,
priv: "priv/gettext",
interpolation: Gettext.Interpolation.Default,
default_domain: "default"
]
|> Keyword.merge(Application.compile_env(@otp_app, __MODULE__, []))
|> Keyword.put_new(
:plural_forms,
Application.compile_env(:gettext, :plural_forms, Gettext.Plural)
)
@priv Keyword.fetch!(@opts, :priv)
@interpolation Keyword.fetch!(@opts, :interpolation)
@default_domain Keyword.fetch!(@opts, :default_domain)
@plural_mod Keyword.fetch!(@opts, :plural_forms)
@snapshot Compiler.snapshot(@opts)
@catalog_bin @snapshot.binary
@catalog_hash :erlang.md5(@snapshot.binary)
@known_locales @snapshot.known_locales
@plural_infos @snapshot.plural_infos
@po_hash @snapshot.hash
for path <- @snapshot.po_paths do
@external_resource path
end
@doc false
def __mix_recompile__? do
@po_hash != Compiler.hash(@opts)
end
@doc false
def __gettext__(:priv), do: @priv
def __gettext__(:otp_app), do: @otp_app
def __gettext__(:known_locales), do: @known_locales
def __gettext__(:default_domain), do: @default_domain
def __gettext__(:interpolation), do: @interpolation
def __gettext__(:default_locale) do
Keyword.get(@opts, :default_locale) || Application.fetch_env!(:gettext, :default_locale)
end
if Gettext.Extractor.extracting?() do
Gettext.ExtractorAgent.add_backend(__MODULE__)
end
@impl Gettext.Backend
def lgettext(locale, domain, msgctxt \\ nil, msgid, bindings)
def lgettext(locale, domain, msgctxt, msgid, bindings) do
case lookup(locale, domain, msgctxt, msgid) do
{:singular, interpolatable} ->
@interpolation.runtime_interpolate(interpolatable, bindings)
{:plural, _msgid_plural, %{0 => interpolatable}, _source} ->
@interpolation.runtime_interpolate(interpolatable, bindings)
_ ->
handle_missing_translation(locale, domain, msgctxt, msgid, bindings)
end
end
@impl Gettext.Backend
def lngettext(locale, domain, msgctxt \\ nil, msgid, msgid_plural, n, bindings)
def lngettext(locale, domain, msgctxt, msgid, msgid_plural, n, bindings) do
case lookup(locale, domain, msgctxt, msgid) do
{:plural, ^msgid_plural, forms, source} ->
interpolate_plural(locale, domain, forms, n, bindings, source)
_ ->
handle_missing_plural_translation(
locale,
domain,
msgctxt,
msgid,
msgid_plural,
n,
bindings
)
end
end
@impl Gettext.Backend
def handle_missing_bindings(exception, incomplete) do
_ = Logger.error(Exception.message(exception))
incomplete
end
@impl Gettext.Backend
def handle_missing_translation(_locale, domain, _msgctxt, msgid, bindings) do
Gettext.Compiler.warn_if_domain_contains_slashes(domain)
with {:ok, interpolated} <- @interpolation.runtime_interpolate(msgid, bindings),
do: {:default, interpolated}
end
@impl Gettext.Backend
def handle_missing_plural_translation(
_locale,
domain,
_msgctxt,
msgid,
msgid_plural,
n,
bindings
) do
Gettext.Compiler.warn_if_domain_contains_slashes(domain)
string = if n == 1, do: msgid, else: msgid_plural
bindings = Map.put(bindings, :count, n)
with {:ok, interpolated} <- @interpolation.runtime_interpolate(string, bindings),
do: {:default, interpolated}
end
defp lookup(locale, domain, msgctxt, msgid) do
catalog()
|> Map.get(locale, %{})
|> Map.get(domain, %{})
|> Map.get({msgctxt, msgid}, :miss)
end
defp interpolate_plural(locale, domain, forms, n, bindings, {file, line}) do
# `plural_info` carries the file's own `Plural-Forms:` header when it has
# one, so a translator-authored rule wins over Gettext's built-in table --
# and `@plural_mod` honours `config :gettext, :plural_forms`.
form = @plural_mod.plural(Map.get(@plural_infos, {locale, domain}, locale), n)
bindings = Map.put(bindings, :count, n)
case forms do
%{^form => interpolatable} ->
@interpolation.runtime_interpolate(interpolatable, bindings)
%{} ->
raise Gettext.PluralFormError,
form: form,
locale: locale,
file: file,
line: line
end
end
@doc """
Decodes the embedded catalogue into `:persistent_term` ahead of the first
lookup.
Called from `PhoenixKit.Application.start/2`. The decode costs ~20ms and
`:persistent_term.put/2` scans every process, so leaving it to the first
`gettext` call puts both on a random request instead of on boot.
"""
@spec warm_catalog() :: :ok
def warm_catalog do
_ = catalog()
:ok
end
# The term is stored WITH the hash of the binary it was decoded from. The
# key outlives the module: a recompile after a `.po` edit (the code
# reloader, IEx `recompile`, a hot upgrade) loads a new `@catalog_bin`, and
# a term cached under a bare key kept answering with the old strings until
# the VM restarted. A mismatch re-decodes and replaces the term — one global
# GC, on a recompile only.
defp catalog do
key = {__MODULE__, :catalog}
case :persistent_term.get(key, :"$miss") do
{@catalog_hash, cat} ->
cat
_stale_or_missing ->
cat = :erlang.binary_to_term(@catalog_bin)
:persistent_term.put(key, {@catalog_hash, cat})
cat
end
end
end