defmodule GettextPseudolocalize.Process do @moduledoc """ Handles the pseudolocalization of strings for gettext translations. This module provides functionality to convert regular strings into pseudolocalized versions while preserving interpolation variables. Pseudolocalization helps identify hardcoded strings, character encoding issues, and UI problems by converting ASCII characters to similar-looking Unicode characters. The conversion: - Preserves gettext interpolation variables (e.g., %{name}) - Converts ASCII characters to accented Unicode equivalents - Wraps the result in brackets (⟦...⟧) for easy identification """ @doc """ Converts a string to its pseudolocalized equivalent. This function preserves gettext interpolation variables while converting regular text to pseudolocalized versions. The result is wrapped in special brackets for easy identification. ## Examples iex> GettextPseudolocalize.Process.convert("Hello") "⟦Ȟêĺĺø~~~~~~~~~~⟧" iex> GettextPseudolocalize.Process.convert("Hello %{name}") "⟦Ȟêĺĺø %{name}~~~~~~⟧" """ def convert(string) when is_binary(string) do string |> split_interpolations() |> Enum.map(&process_part/1) |> IO.iodata_to_binary() |> add_padding() |> wrap_brackets() end defp add_padding(string) do size = length(String.graphemes(string)) padding = if size > 10, do: calculate_padding(size), else: 15 String.pad_trailing(string, padding, "~") end defp calculate_padding(size), do: trunc(Float.ceil(size * 1.4)) defp wrap_brackets(string), do: "⟦#{string}⟧" # Split string into parts using recursive parsing defp split_interpolations(string), do: do_split_interpolations(string, [], "") defp do_split_interpolations("", acc, current), do: Enum.reverse([current | acc]) defp do_split_interpolations("%{" <> rest, acc, current) do acc = [current | acc] {var, rest} = extract_variable(rest) var = IO.iodata_to_binary(["%{", var, "}"]) do_split_interpolations(rest, [var | acc], "") end defp do_split_interpolations(<>, acc, current), do: do_split_interpolations(rest, acc, current <> <>) defp extract_variable(str), do: do_extract_variable(str, []) defp do_extract_variable("}" <> rest, acc), do: {Enum.reverse(acc), rest} defp do_extract_variable(<>, acc), do: do_extract_variable(rest, [<> | acc]) # Process each part - either expand characters or keep as is for interpolations defp process_part(<<"%{", _::binary>> = part), do: part defp process_part(part), do: expand_characters(part) # Map of ASCII characters to their pseudo-localized equivalents @char_map %{ "A" => "Å", "B" => "Ɓ", "C" => "Ċ", "D" => "Đ", "E" => "Ȅ", "F" => "Ḟ", "G" => "Ġ", "H" => "Ȟ", "I" => "İ", "J" => "Ĵ", "K" => "Ǩ", "L" => "Ĺ", "M" => "Ṁ", "N" => "Ñ", "O" => "Ò", "P" => "Ƥ", "Q" => "Ꝗ", "R" => "Ȓ", "S" => "Ș", "T" => "Ť", "U" => "Ü", "V" => "Ṽ", "W" => "Ẃ", "X" => "Ẍ", "Y" => "Ẏ", "Z" => "Ž", "a" => "à", "b" => "ƀ", "c" => "ċ", "d" => "đ", "e" => "ê", "f" => "ƒ", "g" => "ğ", "h" => "ȟ", "i" => "í", "j" => "ǰ", "k" => "ǩ", "l" => "ĺ", "m" => "ɱ", "n" => "ñ", "o" => "ø", "p" => "ƥ", "q" => "ʠ", "r" => "ȓ", "s" => "š", "t" => "ť", "u" => "ü", "v" => "ṽ", "w" => "ẁ", "x" => "ẋ", "y" => "ÿ", "z" => "ź", "0" => "𝟘", "1" => "𝟙", "2" => "𝟚", "3" => "𝟛", "4" => "𝟜", "5" => "𝟝", "6" => "𝟞", "7" => "𝟟", "8" => "𝟠", "9" => "𝟡" } defp expand_characters(string) do String.graphemes(string) |> Enum.map(fn char -> Map.get(@char_map, char, char) end) end end