# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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:

      import Pleroma.Web.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.
  """
  use Gettext, otp_app: :pleroma

  def language_tag do
    # Naive implementation: HTML lang attribute uses BCP 47, which
    # uses - as a separator.
    # https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang

    Gettext.get_locale()
    |> String.replace("_", "-", global: true)
  end

  def normalize_locale(locale) do
    if is_binary(locale) do
      String.replace(locale, "-", "_", global: true)
    else
      nil
    end
  end

  def supports_locale?(locale) do
    Pleroma.Web.Gettext
    |> Gettext.known_locales()
    |> Enum.member?(locale)
  end

  def variant?(locale), do: String.contains?(locale, "_")

  def language_for_variant(locale) do
    Enum.at(String.split(locale, "_"), 0)
  end

  def ensure_fallbacks(locales) do
    locales
    |> Enum.flat_map(fn locale ->
      others =
        other_supported_variants_of_locale(locale)
        |> Enum.filter(fn l -> not Enum.member?(locales, l) end)

      [locale] ++ others
    end)
  end

  def other_supported_variants_of_locale(locale) do
    cond do
      supports_locale?(locale) ->
        []

      variant?(locale) ->
        lang = language_for_variant(locale)
        if supports_locale?(lang), do: [lang], else: []

      true ->
        Gettext.known_locales(Pleroma.Web.Gettext)
        |> Enum.filter(fn l -> String.starts_with?(l, locale <> "_") end)
    end
  end

  def get_locales do
    Process.get({Pleroma.Web.Gettext, :locales}, [])
  end

  def is_locale_list(locales) do
    Enum.all?(locales, &is_binary/1)
  end

  def put_locales(locales) do
    if is_locale_list(locales) do
      Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
      Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
      :ok
    else
      {:error, :not_locale_list}
    end
  end

  def locale_or_default(locale) do
    if supports_locale?(locale) do
      locale
    else
      Gettext.get_locale()
    end
  end

  def with_locales_func(locales, fun) do
    prev_locales = Process.get({Pleroma.Web.Gettext, :locales})
    put_locales(locales)

    try do
      fun.()
    after
      if prev_locales do
        put_locales(prev_locales)
      else
        Process.delete({Pleroma.Web.Gettext, :locales})
        Process.delete(Gettext)
      end
    end
  end

  defmacro with_locales(locales, do: fun) do
    quote do
      Pleroma.Web.Gettext.with_locales_func(unquote(locales), fn ->
        unquote(fun)
      end)
    end
  end

  def to_locale_list(locale) when is_binary(locale) do
    locale
    |> String.split(",")
    |> Enum.filter(&supports_locale?/1)
  end

  def to_locale_list(_), do: []

  defmacro with_locale_or_default(locale, do: fun) do
    quote do
      Pleroma.Web.Gettext.with_locales_func(
        Pleroma.Web.Gettext.to_locale_list(unquote(locale))
        |> Enum.concat(Pleroma.Web.Gettext.get_locales()),
        fn ->
          unquote(fun)
        end
      )
    end
  end

  defp next_locale(locale, list) do
    index = Enum.find_index(list, fn item -> item == locale end)

    if not is_nil(index) do
      Enum.at(list, index + 1)
    else
      nil
    end
  end

  # We do not yet have a proper English translation. The "English"
  # version is currently but the fallback msgid. However, this
  # will not work if the user puts English as the first language,
  # and at the same time specifies other languages, as gettext will
  # think the English translation is missing, and call
  # handle_missing_translation functions. This may result in
  # text in other languages being shown even if English is preferred
  # by the user.
  #
  # To prevent this, we do not allow fallbacking when the current
  # locale missing a translation is English.
  defp should_fallback?(locale) do
    locale != "en"
  end

  def handle_missing_translation(locale, domain, msgctxt, msgid, bindings) do
    next = next_locale(locale, get_locales())

    if is_nil(next) or not should_fallback?(locale) do
      super(locale, domain, msgctxt, msgid, bindings)
    else
      {:ok,
       Gettext.with_locale(next, fn ->
         Gettext.dpgettext(Pleroma.Web.Gettext, domain, msgctxt, msgid, bindings)
       end)}
    end
  end

  def handle_missing_plural_translation(
        locale,
        domain,
        msgctxt,
        msgid,
        msgid_plural,
        n,
        bindings
      ) do
    next = next_locale(locale, get_locales())

    if is_nil(next) or not should_fallback?(locale) do
      super(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
    else
      {:ok,
       Gettext.with_locale(next, fn ->
         Gettext.dpngettext(
           Pleroma.Web.Gettext,
           domain,
           msgctxt,
           msgid,
           msgid_plural,
           n,
           bindings
         )
       end)}
    end
  end
end
