Current section

Files

Jump to
gettext lib gettext po parser.ex
Raw

lib/gettext/po/parser.ex

defmodule Gettext.PO.Parser do
@moduledoc false
alias Gettext.PO.Translation
alias Gettext.PO.PluralTranslation
@doc """
Parses a list of tokens into a list of translations.
"""
@spec parse([Gettext.PO.Tokenizer.token]) ::
{:ok, [binary], [Gettext.PO.translation]} | Gettext.PO.parse_error
def parse(tokens) do
case :gettext_po_parser.parse(tokens) do
{:ok, translations} ->
do_parse(translations)
{:error, _reason} = error ->
parse_error(error)
end
end
defp do_parse(translations) do
translations = Enum.map(translations, &to_struct/1)
case check_for_duplicates(translations) do
{:error, _line, _reason} = error ->
error
:ok ->
{headers, translations} = extract_headers(translations)
{:ok, headers, translations}
end
end
defp to_struct({:translation, translation}),
do: struct(Translation, translation) |> extract_references() |> extract_flags()
defp to_struct({:plural_translation, translation}),
do: struct(PluralTranslation, translation) |> extract_references() |> extract_flags()
defp parse_error({:error, {line, _module, reason}}) do
{:error, line, IO.chardata_to_string(reason)}
end
defp extract_references(%{__struct__: _, comments: comments} = translation) do
{reference_comments, other_comments} = Enum.partition(comments, &match?("#:" <> _, &1))
references =
for "#:" <> contents <- reference_comments,
(contents = String.strip(contents)) != "",
do: parse_reference(contents)
%{translation | references: references, comments: other_comments}
end
defp parse_reference(ref) do
[file, line] = String.split(ref, ":")
{file, String.to_integer(line)}
end
defp extract_flags(%{__struct__: _, comments: comments} = translation) do
{flag_comments, other_comments} = Enum.partition(comments, &match?("#," <> _, &1))
%{translation | flags: parse_flags(flag_comments), comments: other_comments}
end
defp parse_flags(flag_comments) do
flag_comments
|> Stream.map(fn("#," <> content) -> content end)
|> Stream.flat_map(&String.split(&1, ~r/\s+/, trim: true))
|> Enum.into(MapSet.new)
end
# If the first translation has an empty msgid, it's assumed to represent
# headers. Headers will be in the msgstr of this "fake" translation, one on
# each line. For now, we'll just separate those lines in order to get a list
# of headers.
defp extract_headers([%Translation{msgid: id, msgstr: headers}|rest])
when id == "" or id == [""],
do: {headers, rest}
defp extract_headers(translations),
do: {[], translations}
defp check_for_duplicates(translations) do
try do
Enum.reduce translations, HashDict.new, fn(t, acc) ->
id = translation_id(t)
if old_line = Dict.get(acc, id) do
throw({t, old_line})
else
Dict.put_new(acc, id, t.po_source_line)
end
end
:ok
catch
{t, old_line} ->
build_duplicated_error(t, old_line)
end
end
defp translation_id(%Translation{msgid: id}),
do: id
defp translation_id(%PluralTranslation{msgid: id, msgid_plural: idp}),
do: {id, idp}
defp build_duplicated_error(%Translation{} = t, old_line) do
id = IO.iodata_to_binary(t.msgid)
{:error, t.po_source_line, "found duplicate on line #{old_line} for msgid: '#{id}'"}
end
defp build_duplicated_error(%PluralTranslation{} = t, old_line) do
id = IO.iodata_to_binary(t.msgid)
idp = IO.iodata_to_binary(t.msgid_plural)
msg = "found duplicate on line #{old_line} for msgid: '#{id}' and msgid_plural: '#{idp}'"
{:error, t.po_source_line, msg}
end
end