Packages

Multilingual full-text search building blocks for plain Elixir/Ecto: a stemming text pipeline and Postgres tsvector/tsquery helpers. No Ash required.

Current section

Files

Jump to
search_core lib search_core language.ex
Raw

lib/search_core/language.ex

defmodule SearchCore.Language do
@moduledoc """
The languages the search stack accepts: **ISO 639-1 codes**, exactly the set the
installed `text_stemmer` reports — `:fr`, `:en`, `:de`, …
iex> SearchCore.Language.supported?(:fr)
true
iex> SearchCore.Language.supported?(:klingon)
false
There is one spelling for a language and no aliases: `search_core` does not decide what
a language is called, and does not carry a table that could disagree with the stemmer.
Which languages exist is asked of `Text.Stemmer` at call time, never frozen in, so
upgrading `text_stemmer` makes any language it adds usable **immediately** — no
`search_core` release, no recompile.
Need a human-readable, *localized* language name for a UI? That is what the ISO code is
for: hand it to [`ex_cldr_languages`](https://hex.pm/packages/ex_cldr_languages), which
does it properly in every locale. Storing English names here would be a worse copy of a
solved problem.
## Algorithm variants
Snowball ships more than one algorithm for a few languages, addressed by a qualified
code: `:en_porter`, `:en_lovins`, `:nl_porter`. A variant still belongs to its language
`:en_porter` is English, so it takes the English stopword list — which is what
`base/1` is for. Use `base/1`, not the raw code, for anything keyed by *language*
rather than by algorithm.
"""
@type t :: atom()
@doc """
Every language the installed stemmer supports, including algorithm variants.
This is the set to validate user-facing input against, and what `one_of:` constraints
should use.
"""
@spec supported_languages() :: [t()]
def supported_languages, do: Text.Stemmer.supported_languages()
@doc "Whether the installed stemmer supports `lang`."
@spec supported?(any()) :: boolean()
def supported?(lang), do: lang in supported_languages()
@doc """
Return `lang`, raising `ArgumentError` unless the installed stemmer supports it.
iex> SearchCore.Language.validate!(:fr)
:fr
"""
@spec validate!(any()) :: t()
def validate!(lang) do
if supported?(lang) do
lang
else
raise ArgumentError,
"unsupported language #{inspect(lang)}; expected an ISO 639-1 code such as " <>
":fr, or an algorithm variant such as :en_porter. See " <>
"SearchCore.Language.supported_languages/0 for the full list."
end
end
@doc """
The language an algorithm variant belongs to: `:en_porter``:en`. Any other supported
code is returned unchanged; `nil` when unsupported.
iex> SearchCore.Language.base(:en_porter)
:en
iex> SearchCore.Language.base(:fr)
:fr
"""
@spec base(any()) :: t() | nil
def base(lang) do
supported = supported_languages()
if lang in supported, do: base_of(lang, supported)
end
# Variant codes are `<iso>_<algorithm>`. Match the base by string so an unknown prefix
# can never mint an atom; fall back to the variant itself if the base is not supported.
defp base_of(lang, supported) do
case String.split(Atom.to_string(lang), "_", parts: 2) do
[_no_variant] -> lang
[base, _algorithm] -> Enum.find(supported, lang, &(Atom.to_string(&1) == base))
end
end
end