Current section

Files

Jump to
localize lib localize locale provider persistent_term.ex
Raw

lib/localize/locale/provider/persistent_term.ex

defmodule Localize.Locale.Provider.PersistentTerm do
@moduledoc """
A locale data provider that stores locale data in `:persistent_term`.
This provider loads CLDR locale data from `.etf` files in the
`priv/localize/locales/` directory and caches it in `:persistent_term`
for fast, concurrent access without copying.
"""
@behaviour Localize.Locale.Provider
alias Localize.Locale.Provider.Cache
@env apply(Mix, :env, [])
@doc """
Loads locale data for the given locale.
Locale data is resolved in the following order:
1. If a fresh copy is present in the on-disk cache
(see `Localize.Locale.Provider.Cache`), it is returned.
2. In `:dev` and `:test` environments, the locale is generated
from the CLDR source data via
`Localize.Data.Locale.generate_and_transform/1`.
3. Otherwise, the locale is downloaded via
`Localize.Locale.Provider.download_locale/1`, written to the
cache, decoded, and returned.
### Arguments
* `locale` is a locale identifier atom or a `t:Localize.LanguageTag.t/0`.
### Returns
* `{:ok, locale_data}` where `locale_data` is a map of the locale's
CLDR data.
* `{:error, exception}` if the locale data cannot be obtained.
"""
@impl Localize.Locale.Provider
@dialyzer {:nowarn_function, load: 1}
def load(locale) do
locale_id = to_locale_id(locale)
case Cache.get(locale_id) do
{:ok, locale_data} ->
{:ok, locale_data}
{:error, _exception} ->
# Resolve to the canonical CLDR locale ID before trying to
# generate or download. Non-canonical forms like :"pt-BR"
# (which CLDR maps to :pt) would otherwise 404 on the CDN.
canonical_id = resolve_canonical_id(locale_id)
if canonical_id != locale_id do
# Try the cache again with the canonical ID — it may already
# be present under the canonical name.
case Cache.get(canonical_id) do
{:ok, locale_data} -> {:ok, locale_data}
{:error, _} -> load_miss(canonical_id, canonical_id)
end
else
load_miss(locale_id, locale)
end
end
end
defp resolve_canonical_id(locale_id) do
case Localize.validate_locale(locale_id) do
{:ok, %{cldr_locale_id: canonical}} when not is_nil(canonical) -> canonical
_ -> locale_id
end
end
if @env in [:dev, :test] do
defp load_miss(_locale_id, locale) do
locale_string = to_string(locale)
locale_data = apply(Localize.Data.Locale, :generate_and_transform, [locale_string])
{:ok, locale_data}
end
else
defp load_miss(locale_id, _locale) do
if Localize.Locale.Provider.allow_download?(__MODULE__) do
case Localize.Locale.Provider.download_locale(locale_id) do
{:ok, binary} ->
_ = Cache.store(locale_id, binary)
{:ok, :erlang.binary_to_term(binary)}
{:error, exception} ->
{:error, exception}
end
else
{:error,
Localize.LocaleNotFoundInCacheError.exception(
locale_id: locale_id,
path: Cache.path(locale_id)
)}
end
end
end
@doc """
Stores locale data in `:persistent_term`.
### Arguments
* `locale_id` is a locale identifier atom.
* `locale_data` is a map of locale data to store.
### Returns
* `:ok` on success.
* `{:error, reason}` on failure.
"""
@impl Localize.Locale.Provider
def store(locale_id, locale_data) do
locale_key = locale_key(locale_id)
:ok = :persistent_term.put(locale_key, locale_data)
end
@doc """
Returns whether locale data has been loaded into `:persistent_term`.
### Arguments
* `locale` is a locale identifier atom or a `t:Localize.LanguageTag.t/0`.
### Returns
* `true` if the locale data is stored in `:persistent_term`.
* `false` otherwise.
"""
@impl Localize.Locale.Provider
def loaded?(locale) do
locale_id = to_locale_id(locale)
locale_key = locale_key(locale_id)
!!:persistent_term.get(locale_key, nil)
end
@doc """
Retrieves a value from locale data stored in `:persistent_term`.
Navigates the locale data map using the provided list of keys.
When the `:fallback` option is `true` and the key path is not
found, parent locales are searched according to the CLDR locale
inheritance chain.
### Arguments
* `locale` is a locale identifier atom or a `t:Localize.LanguageTag.t/0`.
* `keys` is a list of keys to traverse in the locale data map.
* `options` is a keyword list of options. The default is `[]`.
### Options
* `:fallback` is a boolean. When `true`, parent locales are
searched if the key path is not found in the requested locale.
The default is `false`.
### Returns
* `{:ok, value}` if the key path resolves to a value.
* `{:error, reason}` if the key path cannot be resolved.
"""
@impl Localize.Locale.Provider
def get(locale, keys, _options \\ []) when is_list(keys) do
locale_id = to_locale_id(locale)
case :persistent_term.get(locale_key(locale_id), :localize_locale_not_loaded) do
:localize_locale_not_loaded ->
{:error, Localize.ItemNotFoundError.exception(locale: locale_id, keys: keys)}
locale_data ->
case get_in(locale_data, keys) do
nil -> {:error, Localize.ItemNotFoundError.exception(locale: locale_id, keys: keys)}
item -> {:ok, item}
end
end
end
# ── Helpers ──────────────────────────────────────────────────
defp to_locale_id(locale) do
Localize.Locale.to_locale_id(locale)
end
defp locale_key(locale_id) do
{:localize, locale_id}
end
end