lib/expo/message.ex

defmodule Expo.Message do
  @moduledoc """
  Functions to work on message structs (`Expo.Message.Singular` and `Expo.Message.Plural`).

  A message is a single PO message. For example:

      ```
      msgid "Hello"
      msgstr ""
      ```

  Message structs are used both to represent reference messages (where the `msgstr` is empty)
  in POT files as well as actual translations.
  """

  alias Expo.Message.{Plural, Singular}

  @type msgid :: [String.t()]
  @type msgstr :: [String.t()]
  @type msgctxt :: String.t()

  @type t :: Singular.t() | Plural.t()

  @typedoc """
  The key that can be used to identify a message.

  See `key/1`.
  """
  @opaque key ::
            {msgctxt :: String.t(),
             msgid :: String.t() | {msgid :: String.t(), msgid_plural :: String.t()}}

  @doc """
  Returns a "key" that can be used to identify a message.

  This function returns a "key" that can be used to uniquely identify a
  message assuming that no "same" messages exist; for what "same"
  means, look at the documentation for `same?/2`.

  The purpose of this function is to be used in situations where we'd like to
  group or sort messages but where we don't need the whole structs.

  ## Examples

      iex> t1 = %Expo.Message.Singular{msgid: ["foo"]}
      iex> t2 = %Expo.Message.Singular{msgid: ["", "foo"]}
      iex> Expo.Message.key(t1) == Expo.Message.key(t2)
      true

      iex> t1 = %Expo.Message.Singular{msgid: ["foo"]}
      iex> t2 = %Expo.Message.Singular{msgid: ["bar"]}
      iex> Expo.Message.key(t1) == Expo.Message.key(t2)
      false

  """
  @spec key(t()) :: key()
  def key(message)
  def key(%Singular{} = message), do: Singular.key(message)
  def key(%Plural{} = message), do: Plural.key(message)

  @doc """
  Tells whether two messages are the same message according to their
  `msgid`.

  This function returns `true` if `message1` and `message2` are the same
  message, where "the same" means they have the same `msgid` or the same
  `msgid` and `msgid_plural`.

  ## Examples

      iex> t1 = %Expo.Message.Singular{msgid: ["foo"]}
      iex> t2 = %Expo.Message.Singular{msgid: ["", "foo"]}
      iex> Expo.Message.same?(t1, t2)
      true

      iex> t1 = %Expo.Message.Singular{msgid: ["foo"]}
      iex> t2 = %Expo.Message.Singular{msgid: ["bar"]}
      iex> Expo.Message.same?(t1, t2)
      false

  """
  @spec same?(t(), t()) :: boolean
  def same?(message1, message2), do: key(message1) == key(message2)

  @doc """
  Tells whether the given `message` has the given `flag` specified.

  ### Examples

      iex> Expo.Message.has_flag?(%Expo.Message.Singular{msgid: [], flags: [["foo"]]}, "foo")
      true

      iex> Expo.Message.has_flag?(%Expo.Message.Singular{msgid: [], flags: [["foo"]]}, "bar")
      false

  """
  @spec has_flag?(t(), String.t()) :: boolean()
  def has_flag?(%mod{flags: flags} = _message, flag)
      when mod in [Singular, Plural] and is_binary(flag) do
    flag in List.flatten(flags)
  end

  @doc """
  Appends the given `flag` to the given `message`.

  Keeps the line formatting intact.

  ### Examples

      iex> message = %Expo.Message.Singular{msgid: [], flags: []}
      iex> Expo.Message.append_flag(message, "foo")
      %Expo.Message.Singular{msgid: [], flags: [["foo"]]}

  """
  @spec append_flag(t(), String.t()) :: t()
  def append_flag(%mod{flags: flags} = message, flag) when mod in [Singular, Plural] do
    flags =
      if has_flag?(message, flag) do
        flags
      else
        case flags do
          [] -> [[flag]]
          [flag_line] -> [flag_line ++ [flag]]
          _multiple_lines -> flags ++ [[flag]]
        end
      end

    struct!(message, flags: flags)
  end

  @doc """
  Get the source line number of the message.

  ## Examples

      iex> %Expo.Messages{messages: [message]} = Expo.PO.parse_string!(\"""
      ...> msgid "foo"
      ...> msgstr "bar"
      ...> \""")
      iex> Expo.Message.source_line_number(message, :msgid)
      1

  """
  @spec source_line_number(Singular.t(), Singular.block(), default) :: non_neg_integer() | default
        when default: term
  @spec source_line_number(Plural.t(), Plural.block(), default) :: non_neg_integer() | default
        when default: term
  def source_line_number(%mod{} = message, block, default \\ nil)
      when mod in [Singular, Plural] do
    mod.source_line_number(message, block, default)
  end
end