towerops/lib/mix/tasks/populate_english.ex

234 lines
6.3 KiB
Elixir

defmodule Mix.Tasks.PopulateEnglish do
@shortdoc "Populates English .po files with msgstr = msgid"
@moduledoc """
Populates English translation files by setting msgstr = msgid.
For English-only applications using Gettext, this task automatically
fills in translation strings so that the original English text is used.
## Usage
mix populate_english
This task will:
1. Find all .po files in priv/gettext/en/LC_MESSAGES/
2. For each msgid without a msgstr, set msgstr to equal msgid
3. Preserve existing non-empty msgstr values
Run this after extracting translations with `mix gettext.extract --merge`.
"""
use Mix.Task
@impl Mix.Task
def run(_args) do
po_dir = Path.join(["priv", "gettext", "en", "LC_MESSAGES"])
if !File.dir?(po_dir) do
Mix.raise("Directory not found: #{po_dir}")
end
po_files = Path.wildcard(Path.join(po_dir, "*.po"))
if Enum.empty?(po_files) do
Mix.shell().info("No .po files found in #{po_dir}")
:ok
else
Enum.each(po_files, &process_po_file/1)
Mix.shell().info("✓ Populated #{length(po_files)} English translation file(s)")
end
end
defp process_po_file(file_path) do
content = File.read!(file_path)
updated_content = populate_translations(content)
if content == updated_content do
Mix.shell().info(" No changes: #{Path.basename(file_path)}")
else
File.write!(file_path, updated_content)
Mix.shell().info(" Updated: #{Path.basename(file_path)}")
end
end
defp populate_translations(content) do
# Match msgid followed by msgstr
# Handle both single-line and multi-line strings
content
|> String.split("\n")
|> process_lines([])
|> Enum.reverse()
|> Enum.join("\n")
end
defp process_lines([], acc), do: acc
defp process_lines([line | rest], acc) do
if String.starts_with?(line, "msgid ") do
process_msgid_line([line | rest], acc)
else
# Regular line (comments, metadata, blank lines)
process_lines(rest, [line | acc])
end
end
defp process_msgid_line(lines, acc) do
{msgid_lines, remaining} = collect_string_lines(lines, [])
# Check if this is a plural form (msgid_plural follows)
if plural_form?(remaining) do
process_plural_form(msgid_lines, remaining, acc)
else
process_singular_form(msgid_lines, remaining, acc)
end
end
defp process_plural_form(msgid_lines, remaining, acc) do
{plural_lines, remaining2} = skip_plural_translation(remaining, [])
new_acc = Enum.reverse(plural_lines, Enum.reverse(msgid_lines, acc))
process_lines(remaining2, new_acc)
end
defp process_singular_form(msgid_lines, remaining, acc) do
{msgstr_lines, remaining2} = collect_msgstr_lines(remaining, [])
msgid_value = extract_string_value(msgid_lines)
updated_msgstr =
if should_populate?(msgid_value, msgstr_lines) do
populate_msgstr(msgid_lines, msgid_value)
else
msgstr_lines
end
new_acc = Enum.reverse(updated_msgstr, Enum.reverse(msgid_lines, acc))
process_lines(remaining2, new_acc)
end
# Collect lines that are part of a msgid string (including continuation lines)
defp collect_string_lines([], acc), do: {Enum.reverse(acc), []}
defp collect_string_lines([line | rest] = lines, acc) do
cond do
String.starts_with?(line, "msgid ") ->
collect_string_lines(rest, [line | acc])
String.starts_with?(String.trim_leading(line), "\"") and acc != [] ->
# Continuation line
collect_string_lines(rest, [line | acc])
true ->
{Enum.reverse(acc), lines}
end
end
# Collect msgstr lines
defp collect_msgstr_lines([], acc), do: {Enum.reverse(acc), []}
defp collect_msgstr_lines([line | rest] = lines, acc) do
cond do
String.starts_with?(line, "msgstr ") ->
collect_msgstr_lines(rest, [line | acc])
String.starts_with?(String.trim_leading(line), "\"") and acc != [] ->
# Continuation line
collect_msgstr_lines(rest, [line | acc])
true ->
{Enum.reverse(acc), lines}
end
end
# Extract the actual string value from msgid/msgstr lines
defp extract_string_value(lines) do
Enum.map_join(lines, &extract_line_value/1)
end
defp extract_line_value(line) do
# Remove msgid/msgstr prefix and extract quoted content
line
|> String.replace(~r/^msgid\s+/, "")
|> String.replace(~r/^msgstr\s+/, "")
|> String.trim()
|> extract_quoted_string()
end
defp extract_quoted_string(s) do
if String.starts_with?(s, "\"") and String.ends_with?(s, "\"") do
s
|> String.slice(1..-2//1)
|> unescape_string()
else
""
end
end
defp unescape_string(s) do
s
|> String.replace("\\n", "\n")
|> String.replace("\\t", "\t")
|> String.replace("\\\"", "\"")
|> String.replace("\\\\", "\\")
end
defp format_string_value(value) do
escaped =
value
|> String.replace("\\", "\\\\")
|> String.replace("\"", "\\\"")
|> String.replace("\n", "\\n")
|> String.replace("\t", "\\t")
"\"#{escaped}\""
end
defp msgstr_empty?([]), do: true
defp msgstr_empty?(lines) do
value = extract_string_value(lines)
String.trim(value) == ""
end
defp should_populate?(msgid_value, msgstr_lines) do
String.trim(msgid_value) != "" and msgstr_empty?(msgstr_lines)
end
defp populate_msgstr(msgid_lines, msgid_value) do
if length(msgid_lines) > 1 do
create_multiline_msgstr(msgid_lines)
else
["msgstr #{format_string_value(msgid_value)}"]
end
end
defp create_multiline_msgstr(msgid_lines) do
msgid_lines
|> Enum.with_index()
|> Enum.map(fn {line, index} ->
if index == 0 do
String.replace(line, ~r/^msgid\s+/, "msgstr ")
else
line
end
end)
end
defp plural_form?([]), do: false
defp plural_form?([line | _rest]) do
String.starts_with?(line, "msgid_plural ")
end
defp skip_plural_translation([], acc), do: {Enum.reverse(acc), []}
defp skip_plural_translation([line | rest] = lines, acc) do
# Collect msgid_plural and msgstr[N] lines
if String.starts_with?(line, "msgid_plural ") or
String.starts_with?(line, "msgstr[") or
(String.starts_with?(String.trim_leading(line), "\"") and acc != []) do
skip_plural_translation(rest, [line | acc])
else
{Enum.reverse(acc), lines}
end
end
end