234 lines
6.3 KiB
Elixir
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
|