Gettext_sigils - a sigil for using gettext with less boilerplate and better readability

Thanks for your feedback! I really appreciate critical feedback. I was not aware of the early language wars and (maybe naively) just picked what looked best (at least to me).

I actually considered using ~GTXT or ~GT for clarity reasons, but decided against it because my goal was keeping the visual noise as low as possible. I wanted to avoid things like this:

~GTXT"No"
# compared to
~t"No"

I also remembered in Rails you could use t("...") and I liked the parallel. I can also say that because we have already used the ~t sigil over several years internally at my company, it’s something you get used to very quickly and everybody in the team recognises this as a translation. But that, of course, is just us in our small bubble and your concern is still valid.

All that said, I’m open for suggestions for 1.x :slight_smile:

Cheers! :heart:

2 Likes

You’ll lose syntax highlighting then. I would even go further and say the sigil should be ~GETTEXT

It would be clear to everyone reading the code what is happening here and also for other tools working on the AST.

2 Likes

Oh wow, thanks for this, I really look forward to using your library! As a French-speaking Swiss dev, the most annoying part is now to find translators :sweat_smile:

1 Like

Quick update: I just released v0.5.0 of this library, with support for custom modifiers!

In addition to the existing domain/context modifier keyword options, it is now possible to define custom modifiers as modules that implement the GettextSigils.Modifier behaviour.

For example, custom modifiers can be used to postprocess the translated message:

defmodule MyApp.MarkdownModifier do
  use GettextSigils.Modifier

  @impl true
  def postprocess(string, opts), 
    do: MDEx.to_html(string, opts)
end

defmodule MyApp.RawModifier do
  use GettextSigils.Modifier

  @impl true
  def postprocess(string, _opts) do 
    {:ok, Phoenix.HTML.raw(string)}
  end
end

The behaviour can also be used to implement custom pluralization rules (I recommend sticking with the default N modifier, though).

defmodule MyApp.SplitPluralModifier do
  use GettextSigils.Modifier

  @schema NimbleOptions.new!(
    separator: [
      type: :string,
      default: "|",
      doc: "Separator between singular and plural forms in the msgid."
    ]
  )

  @impl true
  def init(opts), do: NimbleOptions.validate(opts, @schema)

  @impl true
  def pluralize({msgid, bindings}, opts) do
    case Keyword.pop(bindings, :count) do
      {nil, _} ->
        {:error, ~s|`n` modifier requires a "count" binding|}

      {count, remaining} ->
        separator = Keyword.fetch!(opts, :separator)

        case String.split(msgid, separator, parts: 2) do
          [singular, plural] ->
            {:ok, {singular, plural, count, remaining}}

          _ ->
            {:error, ~s|`n` modifier requires msgid in the form "singular#{separator}plural"|}
        end
    end
  end
end

Then configure GettextSigils to use the modifiers:

use GettextSigils,
  backend: MyApp.Gettext,
  sigils: [
    modifiers: [
      e: [domain: "errors"],
      r: MyApp.RawModifier,
      m: {MyApp.MarkdownModifier, extension: [strikethrough: true]},
      n: MyApp.SplitPluralModifier
    ]
  ]

~t"**One** error|**#{count}** errors"mrn
# => {:safe, "<p><strong>5</strong> errors</p>"}

Thanks to @ErikNaslund for opening an issue that eventually lead to this! :slight_smile:

1 Like

I just updated to 0.5.0 and rewrote my project to use my RawModifier and StylingModifier (markdown-like), instead of raw/1 and custom gettext interpolations. Works like a charm and was really easy to do. Great work @hwuethrich !

Nah, its mostly a probleem with tags in tags.


<div>This should be <b>one</> translatable string</div>

Thanks for your feedback!

Just to be clear, the behaviour was added to give users the option to implement what they think works best for them. Some probably prefer using raw(~t"...") vs. a modifier that does the same. Others would probably make the case that markdown is easier to understand by a translator than raw HTML. The library now supports both.

The behaviour is also used internally for keyword-based modifiers (domain/context) and pluralization, but the library will probably not ship with anything that goes beyond that.

Thanks for your reply.

I was referring to my own proof of concept-attempt you responded to, not the current library. The explicitness is a key factor to keep things sane but it would be super if you could just set a HEEX-flag and all text in the HEEX tempates would become translatable (this would need some serious work, but everyone is free to be inspired…)

if you could just set a HEEX-flag and all text in the HEEX tempates

Unicode Message Format 2 attempts to solve part of this by having structured markup in the message format. The idea being it should be HTML safe, understandable and editable by translators, and reasonably easy to render back to raw HTML.

The markup syntax is parsed correctly by Localize.Message and it can be emitted as a list of kind-of-AST with the interpolations done.

The next step will be to design the best way to integrate the HTML rendering into a HEEX/LiveView environment - and thats not my skill. Collaboration would be very welcome.

And for sure I will spend some time seeing how Localize.Message can integrate with gettext_sigils.

2 Likes

Sorry, I misunderstood your reply! :+1: