From 1eff8d67c642e7e3fae839c81727be1d68eea7e7 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 17 Aug 2024 15:59:59 +0200 Subject: [PATCH 1/3] Avoid compile-time dependency to Gettext backend --- lib/gettext.ex | 33 +++- lib/gettext/macros.ex | 239 ++++++++++++++++++++++++ mix.exs | 4 + test/gettext/new_backend_setup_test.exs | 62 ++++++ test/mix/tasks/gettext.extract_test.exs | 31 +-- test/support/mix_project_helpers.ex | 31 +++ 6 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 lib/gettext/macros.ex create mode 100644 test/gettext/new_backend_setup_test.exs create mode 100644 test/support/mix_project_helpers.ex diff --git a/lib/gettext.ex b/lib/gettext.ex index 73eb3a6..b725eb9 100644 --- a/lib/gettext.ex +++ b/lib/gettext.ex @@ -627,19 +627,36 @@ defmodule Gettext do @doc false defmacro __using__(opts) do - quote do - opts = unquote(opts) - - if Keyword.has_key?(opts, :backend) do - raise "not implemented yet" + opts = + if Macro.quoted_literal?(opts) do + Macro.prewalk(opts, &expand_alias(&1, __CALLER__)) else - # TODO: Deprecate this branch - require Gettext.Backend - Gettext.Backend.__using__(opts) + opts end + + case Keyword.fetch(opts, :backend) do + {:ok, backend} -> + quote do + @__gettext_backend__ unquote(backend) + import Gettext.Macros + end + + :error -> + quote do + # TODO: Deprecate this branch + use Gettext.Backend, unquote(opts) + end end end + defp expand_alias({:__aliases__, _, _} = als, env) do + Macro.expand(als, %{env | function: {:__gettext__, 1}}) + end + + defp expand_alias(other, _env) do + other + end + @doc """ Gets the global Gettext locale for the current process. diff --git a/lib/gettext/macros.ex b/lib/gettext/macros.ex new file mode 100644 index 0000000..156ccec --- /dev/null +++ b/lib/gettext/macros.ex @@ -0,0 +1,239 @@ +defmodule Gettext.Macros do + @moduledoc false + + defmacro dpgettext_noop(domain, msgctxt, msgid) do + domain = expand_domain(domain, __CALLER__) + msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__) + msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__) + + if Gettext.Extractor.extracting?() do + Gettext.Extractor.extract( + __CALLER__, + __MODULE__, + domain, + msgctxt, + msgid, + Gettext.Compiler.get_and_flush_extracted_comments() + ) + end + + msgid + end + + defmacro dgettext_noop(domain, msgid) do + quote do + unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid)) + end + end + + defmacro gettext_noop(msgid) do + quote do + unquote(__MODULE__).dpgettext_noop( + {@__gettext_backend__, :__default_domain__}, + nil, + unquote(msgid) + ) + end + end + + defmacro pgettext_noop(msgid, context) do + quote do + unquote(__MODULE__).dpgettext_noop( + {@__gettext_backend__, :__default_domain__}, + unquote(context), + unquote(msgid) + ) + end + end + + defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do + domain = + case domain do + {backend, :__default_domain__} when not is_nil(backend) -> + Macro.expand(backend, __CALLER__).__gettext__(:default_domain) + + _other -> + Gettext.Compiler.expand_to_binary(domain, "domain", __MODULE__, __CALLER__) + end + + msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__) + msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__) + + msgid_plural = + Gettext.Compiler.expand_to_binary(msgid_plural, "msgid_plural", __MODULE__, __CALLER__) + + if Gettext.Extractor.extracting?() do + Gettext.Extractor.extract( + __CALLER__, + __MODULE__, + domain, + msgctxt, + {msgid, msgid_plural}, + Gettext.Compiler.get_and_flush_extracted_comments() + ) + end + + {msgid, msgid_plural} + end + + defmacro dngettext_noop(domain, msgid, msgid_plural) do + quote do + unquote(__MODULE__).dpngettext_noop( + unquote(domain), + nil, + unquote(msgid), + unquote(msgid_plural) + ) + end + end + + defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do + quote do + unquote(__MODULE__).dpngettext_noop( + {@__gettext_backend__, :__default_domain__}, + unquote(msgctxt), + unquote(msgid), + unquote(msgid_plural) + ) + end + end + + defmacro ngettext_noop(msgid, msgid_plural) do + quote do + unquote(__MODULE__).dpngettext_noop( + {@__gettext_backend__, :__default_domain__}, + nil, + unquote(msgid), + unquote(msgid_plural) + ) + end + end + + defmacro dpgettext(domain, msgctxt, msgid, bindings \\ Macro.escape(%{})) do + domain = expand_domain(domain, __CALLER__) + + quote do + msgid = + unquote(__MODULE__).dpgettext_noop(unquote(domain), unquote(msgctxt), unquote(msgid)) + + Gettext.dpgettext( + @__gettext_backend__, + unquote(domain), + unquote(msgctxt), + msgid, + unquote(bindings) + ) + end + end + + defmacro dgettext(domain, msgid, bindings \\ Macro.escape(%{})) do + quote do + unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings)) + end + end + + defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do + quote do + unquote(__MODULE__).dpgettext( + {@__gettext_backend__, :__default_domain__}, + unquote(msgctxt), + unquote(msgid), + unquote(bindings) + ) + end + end + + defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do + quote do + unquote(__MODULE__).dpgettext( + {@__gettext_backend__, :__default_domain__}, + nil, + unquote(msgid), + unquote(bindings) + ) + end + end + + defmacro dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do + domain = expand_domain(domain, __CALLER__) + + quote do + {msgid, msgid_plural} = + unquote(__MODULE__).dpngettext_noop( + unquote(domain), + unquote(msgctxt), + unquote(msgid), + unquote(msgid_plural) + ) + + Gettext.dpngettext( + @__gettext_backend__, + unquote(domain), + unquote(msgctxt), + msgid, + msgid_plural, + unquote(n), + unquote(bindings) + ) + end + end + + defmacro dngettext(domain, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do + quote do + unquote(__MODULE__).dpngettext( + unquote(domain), + nil, + unquote(msgid), + unquote(msgid_plural), + unquote(n), + unquote(bindings) + ) + end + end + + defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do + quote do + unquote(__MODULE__).dpngettext( + {@__gettext_backend__, :__default_domain__}, + nil, + unquote(msgid), + unquote(msgid_plural), + unquote(n), + unquote(bindings) + ) + end + end + + defmacro pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do + quote do + unquote(__MODULE__).dpngettext( + {@__gettext_backend__, :__default_domain__}, + unquote(msgctxt), + unquote(msgid), + unquote(msgid_plural), + unquote(n), + unquote(bindings) + ) + end + end + + defmacro gettext_comment(comment) do + comment = Gettext.Compiler.expand_to_binary(comment, "comment", __MODULE__, __CALLER__) + Gettext.Compiler.append_extracted_comment(comment) + :ok + end + + defp expand_domain({backend, :__default_domain__}, env) do + if Gettext.Extractor.extracting?() do + Macro.expand(backend, env).__gettext__(:default_domain) + else + quote do + unquote(backend).__gettext__(:default_domain) + end + end + end + + defp expand_domain(domain, _env) do + domain + end +end diff --git a/mix.exs b/mix.exs index afcc96e..393883f 100644 --- a/mix.exs +++ b/mix.exs @@ -11,6 +11,7 @@ defmodule Gettext.Mixfile do app: :gettext, version: @version, elixir: "~> 1.12", + elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, deps: deps(), preferred_cli_env: [coveralls: :test, "coveralls.html": :test, "coveralls.github": :test], @@ -47,6 +48,9 @@ defmodule Gettext.Mixfile do ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_other), do: ["lib"] + defp deps do [ {:expo, "~> 0.5.1 or ~> 1.0"}, diff --git a/test/gettext/new_backend_setup_test.exs b/test/gettext/new_backend_setup_test.exs new file mode 100644 index 0000000..e523b1a --- /dev/null +++ b/test/gettext/new_backend_setup_test.exs @@ -0,0 +1,62 @@ +defmodule Gettext.NewBackendSetupTest do + # https://github.com/elixir-gettext/gettext/issues/330 + use ExUnit.Case, async: true + + import ExUnit.CaptureIO + import GettextTest.MixProjectHelpers + + @moduletag :tmp_dir + + defmodule Backend do + use Gettext.Backend, otp_app: :test_application + end + + describe "use Gettext, backend: ..." do + test "imports Gettext.Macros macros but doesn't generate functions" do + {{:module, mod, _bytecode, _funs}, _bindings} = + Code.eval_quoted( + quote do + defmodule MyModule do + use Gettext, + backend: Gettext.NewBackendSetupTest.Backend + + def translate do + gettext("Hello world") + end + end + end + ) + + refute function_exported?(mod, :__gettext__, 1) + + assert mod.translate() == "Hello world" + end + end + + describe "compile-time dependencies" do + @tag :skip + test "are not created for modules that use the backend", + %{test: test, tmp_dir: tmp_dir} = context do + create_test_mix_file(context) + + write_file(context, "lib/my_app.ex", """ + defmodule MyApp.Gettext do + use Gettext.Backend, otp_app: #{inspect(test)} + end + + defmodule MyApp do + use Gettext, backend: MyApp.Gettext + end + """) + + output = + in_project(test, tmp_dir, fn _module -> + capture_io(fn -> Mix.Task.run("compile") end) + capture_io(fn -> Mix.Task.run("xref", ["trace", "lib/my_app.ex"]) end) + end) + + assert output =~ ~r"lib/my_app\.ex:\d+: alias MyApp\.Gettext \(runtime\)\n" + refute output =~ ~r"lib/my_app\.ex:\d+: alias MyApp\.Gettext \(compile\)\n" + end + end +end diff --git a/test/mix/tasks/gettext.extract_test.exs b/test/mix/tasks/gettext.extract_test.exs index ee35420..c56fe15 100644 --- a/test/mix/tasks/gettext.extract_test.exs +++ b/test/mix/tasks/gettext.extract_test.exs @@ -2,6 +2,7 @@ defmodule Mix.Tasks.Gettext.ExtractTest do use ExUnit.Case import ExUnit.CaptureIO + import GettextTest.MixProjectHelpers @moduletag :tmp_dir @@ -11,10 +12,6 @@ defmodule Mix.Tasks.Gettext.ExtractTest do :ok end - defp in_project(module, dir, fun) do - Mix.Project.in_project(module, dir, [prune_code_paths: false], fun) - end - test "extracting and extracting with --merge", %{test: test, tmp_dir: tmp_dir} = context do create_test_mix_file(context) @@ -188,32 +185,6 @@ defmodule Mix.Tasks.Gettext.ExtractTest do end) end - defp create_test_mix_file(context, gettext_config \\ []) do - write_file(context, "mix.exs", """ - defmodule MyApp.MixProject do - use Mix.Project - - def project() do - [app: #{inspect(context.test)}, version: "0.1.0", gettext: #{inspect(gettext_config)}] - end - - def application() do - [extra_applications: [:logger, :gettext]] - end - end - """) - end - - defp write_file(context, path, contents) do - path = Path.join(context.tmp_dir, path) - File.mkdir_p!(Path.dirname(path)) - File.write!(path, contents) - end - - defp read_file(context, path) do - context.tmp_dir |> Path.join(path) |> File.read!() - end - defp run(args) do Mix.Tasks.Gettext.Extract.run(args) end diff --git a/test/support/mix_project_helpers.ex b/test/support/mix_project_helpers.ex new file mode 100644 index 0000000..4e6c427 --- /dev/null +++ b/test/support/mix_project_helpers.ex @@ -0,0 +1,31 @@ +defmodule GettextTest.MixProjectHelpers do + def create_test_mix_file(context, gettext_config \\ []) do + write_file(context, "mix.exs", """ + defmodule MyApp.MixProject do + use Mix.Project + + def project() do + [app: #{inspect(context.test)}, version: "0.1.0", gettext: #{inspect(gettext_config)}] + end + + def application() do + [extra_applications: [:logger, :gettext]] + end + end + """) + end + + def write_file(context, path, contents) do + path = Path.join(context.tmp_dir, path) + File.mkdir_p!(Path.dirname(path)) + File.write!(path, contents) + end + + def read_file(context, path) do + context.tmp_dir |> Path.join(path) |> File.read!() + end + + def in_project(module, dir, fun) do + Mix.Project.in_project(module, dir, [prune_code_paths: false], fun) + end +end From faf0c97218de115973449d5b519939a7f25bacde Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sun, 18 Aug 2024 10:21:25 +0200 Subject: [PATCH 2/3] Docs and move stuff --- lib/gettext.ex | 4 +- lib/gettext/backend.ex | 269 ++++------------------------------------ lib/gettext/compiler.ex | 6 +- lib/gettext/macros.ex | 210 +++++++++++++++++++++++++++++-- 4 files changed, 231 insertions(+), 258 deletions(-) diff --git a/lib/gettext.ex b/lib/gettext.ex index b725eb9..e6f021b 100644 --- a/lib/gettext.ex +++ b/lib/gettext.ex @@ -634,14 +634,14 @@ defmodule Gettext do opts end - case Keyword.fetch(opts, :backend) do + case Keyword.keyword?(opts) && Keyword.fetch(opts, :backend) do {:ok, backend} -> quote do @__gettext_backend__ unquote(backend) import Gettext.Macros end - :error -> + _other -> quote do # TODO: Deprecate this branch use Gettext.Backend, unquote(opts) diff --git a/lib/gettext/backend.ex b/lib/gettext/backend.ex index aa2cb1f..19b27c2 100644 --- a/lib/gettext/backend.ex +++ b/lib/gettext/backend.ex @@ -175,255 +175,34 @@ defmodule Gettext.Backend do {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]} @doc """ - Translates the given `msgid` with a given context (`msgctxt`) in the given `domain`. + Translates a message. - `bindings` is a map of bindings to support interpolation. - - See also `Gettext.dpgettext/5`. - """ - @macrocallback dpgettext( - domain :: Macro.t(), - msgctxt :: String.t(), - msgid :: String.t(), - bindings :: Macro.t() - ) :: Macro.t() - - @doc """ - Same as `dpgettext(domain, msgctxt, msgid, %{})`. - - See also `Gettext.dpgettext/5`. - """ - @macrocallback dpgettext(domain :: Macro.t(), msgctxt :: String.t(), msgid :: String.t()) :: - Macro.t() - - @doc """ - Translates the given `msgid` in the given `domain`. - - `bindings` is a map of bindings to support interpolation. - - See also `Gettext.dgettext/4`. - """ - @macrocallback dgettext(domain :: Macro.t(), msgid :: String.t(), bindings :: Macro.t()) :: - Macro.t() - - @doc """ - Same as `dgettext(domain, msgid, %{})`. - - See also `Gettext.dgettext/4`. - """ - @macrocallback dgettext(domain :: Macro.t(), msgid :: String.t()) :: Macro.t() - - @doc """ - Translates the given `msgid` with the given context (`msgctxt`). - - `bindings` is a map of bindings to support interpolation. - - See also `Gettext.pgettext/4`. - """ - @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t(), bindings :: Macro.t()) :: - Macro.t() - - @doc """ - Same as `pgettext(msgctxt, msgid, %{})`. - - See also `Gettext.pgettext/4`. - """ - @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t()) :: Macro.t() - - @doc """ - Same as `dgettext("default", msgid, %{})`, but will use a per-backend - configured default domain if provided. - - See also `Gettext.gettext/3`. - """ - @macrocallback gettext(msgid :: String.t(), bindings :: Macro.t()) :: Macro.t() - - @doc """ - Same as `gettext(msgid, %{})`. - - See also `Gettext.gettext/3`. - """ - @macrocallback gettext(msgid :: String.t()) :: Macro.t() - - @doc """ - Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`) - in the given `domain`. - - `n` is an integer used to determine how to pluralize the - message. `bindings` is a map of bindings to support interpolation. - - See also `Gettext.dpngettext/7`. - """ - @macrocallback dpngettext( - domain :: Macro.t(), - msgctxt :: String.t(), - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t(), - bindings :: Macro.t() - ) :: Macro.t() - - @doc """ - Same as `dpngettext(domain, msgctxt, msgid, msgid_plural, n, %{})`. - - See also `Gettext.dpngettext/7`. - """ - @macrocallback dpngettext( - domain :: Macro.t(), - msgctxt :: String.t(), - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t() - ) :: Macro.t() - - @doc """ - Translates the given plural message (`msgid` + `msgid_plural`) in the - given `domain`. - - `n` is an integer used to determine how to pluralize the - message. `bindings` is a map of bindings to support interpolation. - - See also `Gettext.dngettext/6`. - """ - @macrocallback dngettext( - domain :: Macro.t(), - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t(), - bindings :: Macro.t() - ) :: Macro.t() - - @doc """ - Same as `dngettext(domain, msgid, msgid_plural, n, %{})`. - - See also `Gettext.dngettext/6`. - """ - @macrocallback dngettext( - domain :: Macro.t(), - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t() - ) :: Macro.t() - - @doc """ - Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`). - - `n` is an integer used to determine how to pluralize the - message. `bindings` is a map of bindings to support interpolation. - - See also `Gettext.pngettext/6`. + See `Gettext.gettext/3` for more information. """ - @macrocallback pngettext( - msgctxt :: String.t(), - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t(), - bindings :: Macro.t() - ) :: Macro.t() - - @doc """ - Same as `pngettext(msgctxt, msgid, msgid_plural, n, %{})`. - - See also `Gettext.pngettext/6`. - """ - @macrocallback pngettext( - msgctxt :: String.t(), - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t() - ) :: Macro.t() - - @doc """ - Same as `dngettext("default", msgid, msgid_plural, n, bindings)`, but will - use a per-backend configured default domain if provided. - - See also `Gettext.ngettext/5`. - """ - @macrocallback ngettext( - msgid :: String.t(), - msgid_plural :: String.t(), - n :: Macro.t(), - bindings :: Macro.t() - ) :: Macro.t() - - @doc """ - Same as `ngettext(msgid, msgid_plural, n, %{})`. - - See also `Gettext.ngettext/5`. - """ - @macrocallback ngettext(msgid :: String.t(), msgid_plural :: String.t(), n :: Macro.t()) :: - Macro.t() - - @doc """ - Marks the given message for extraction and returns it unchanged. - - This macro can be used to mark a message for extraction when `mix - gettext.extract` is run. The return value is the given string, so that this - macro can be used seamlessly in place of the string to extract. - - ## Examples - - MyApp.Gettext.dgettext_noop("errors", "Error found!") - #=> "Error found!" - - """ - @macrocallback dgettext_noop(domain :: String.t(), msgid :: String.t()) :: Macro.t() - - @doc """ - Same as `dgettext_noop("default", msgid)`. - """ - @macrocallback gettext_noop(msgid :: String.t()) :: Macro.t() - - @doc """ - Marks the given message for extraction and returns - `{msgid, msgid_plural}`. - - This macro can be used to mark a message for extraction when `mix - gettext.extract` is run. The return value of this macro is `{msgid, - msgid_plural}`. - - ## Examples - - my_fun = fn {msgid, msgid_plural} -> - # do something with msgid and msgid_plural - end - - my_fun.(MyApp.Gettext.dngettext_noop("errors", "One error", "%{count} errors")) - - """ - @macrocallback dngettext_noop( - domain :: Macro.t(), - msgid :: String.t(), - msgid_plural :: String.t() - ) :: Macro.t() - - @doc """ - Same as `dngettext_noop("default", msgid, mgsid_plural)`, but will use a - per-backend configured default domain if provided. - """ - @macrocallback ngettext_noop(msgid :: String.t(), msgid_plural :: String.t()) :: Macro.t() + @doc since: "0.26.0" + @callback lgettext( + Gettext.locale(), + domain :: String.t(), + msgctxt :: String.t() | nil, + msgid :: String.t(), + bindings :: map() + ) :: + {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]} @doc """ - Stores an "extracted comment" for the next message. - - This macro can be used to add comments (Gettext refers to such - comments as *extracted comments*) to the next message that will - be extracted. Extracted comments will be prefixed with `#.` in POT - files. - - Calling this function multiple times will accumulate the comments; - when another Gettext macro (such as `c:gettext/2`) is called, - the comments will be extracted and attached to that message, and - they will be flushed so as to start again. - - This macro always returns `:ok`. - - ## Examples - - MyApp.Gettext.gettext_comment("The next message is awesome") - MyApp.Gettext.gettext_comment("Another comment for the next message") - MyApp.Gettext.gettext("The awesome message") + Translates a plural message. + See `Gettext.ngettext/5` for more information. """ - @macrocallback gettext_comment(comment :: String.t()) :: :ok + @doc since: "0.26.0" + @callback lngettext( + Gettext.locale(), + domain :: String.t(), + msgctxt :: String.t() | nil, + msgid :: String.t(), + msgid_plural :: String.t(), + n :: non_neg_integer(), + bindings :: map() + ) :: + {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]} end diff --git a/lib/gettext/compiler.ex b/lib/gettext/compiler.ex index 5c5d43f..aa58cda 100644 --- a/lib/gettext/compiler.ex +++ b/lib/gettext/compiler.ex @@ -63,8 +63,12 @@ defmodule Gettext.Compiler do unquote(macros()) - # These are the two functions we generated inside the backend. + # These are the two functions we generate inside the backend. + + @impl Gettext.Backend def lgettext(locale, domain, msgctxt \\ nil, msgid, bindings) + + @impl Gettext.Backend def lngettext(locale, domain, msgctxt \\ nil, msgid, msgid_plural, n, bindings) unquote(compile_po_files(env, known_po_files, opts)) diff --git a/lib/gettext/macros.ex b/lib/gettext/macros.ex index 156ccec..2e34bdc 100644 --- a/lib/gettext/macros.ex +++ b/lib/gettext/macros.ex @@ -1,6 +1,37 @@ defmodule Gettext.Macros do - @moduledoc false + @moduledoc """ + Macros used by Gettext to provide the gettext family of functions. + *Available since v0.26.0.* + + Macros enable users to use gettext and get **automatic extraction** of translations. + See `Gettext` for more information. + + This module is *imported* every time you call: + + use Gettext, backend: MyApp.Gettext + + > #### Warning {: .error} + > + > You are not meant to use this module in any way other than with `use Gettext` as shown above, + > as its macros depend on internals that get set up when you call `use Gettext`. + """ + + @moduledoc since: "0.26.0" + + @doc """ + Marks the given message for extraction and returns it unchanged. + + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value is the given string, so that this + macro can be used seamlessly in place of the string to extract. + + ## Examples + + dpgettext_noop("errors", "Home page", "Error found!") + #=> "Error found!" + + """ defmacro dpgettext_noop(domain, msgctxt, msgid) do domain = expand_domain(domain, __CALLER__) msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__) @@ -20,12 +51,38 @@ defmodule Gettext.Macros do msgid end + @doc """ + Marks the given message for extraction and returns it unchanged. + + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value is the given string, so that this + macro can be used seamlessly in place of the string to extract. + + ## Examples + + dgettext_noop("errors", "Error found!") + #=> "Error found!" + + """ defmacro dgettext_noop(domain, msgid) do quote do unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid)) end end + @doc """ + Marks the given message for extraction and returns it unchanged. + + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value is the given string, so that this + macro can be used seamlessly in place of the string to extract. + + ## Examples + + gettext_noop("Error found!") + #=> "Error found!" + + """ defmacro gettext_noop(msgid) do quote do unquote(__MODULE__).dpgettext_noop( @@ -36,6 +93,19 @@ defmodule Gettext.Macros do end end + @doc """ + Marks the given message for extraction and returns it unchanged. + + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value is the given string, so that this + macro can be used seamlessly in place of the string to extract. + + ## Examples + + pgettext_noop("Error found!", "Home page") + #=> "Error found!" + + """ defmacro pgettext_noop(msgid, context) do quote do unquote(__MODULE__).dpgettext_noop( @@ -46,16 +116,21 @@ defmodule Gettext.Macros do end end - defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do - domain = - case domain do - {backend, :__default_domain__} when not is_nil(backend) -> - Macro.expand(backend, __CALLER__).__gettext__(:default_domain) + @doc """ + Marks the given message for extraction and returns it unchanged. - _other -> - Gettext.Compiler.expand_to_binary(domain, "domain", __MODULE__, __CALLER__) - end + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value is the given string, so that this + macro can be used seamlessly in place of the string to extract. + + ## Examples + + dpngettext_noop("errors", "Home page", "Error found!", "Errors found!") + #=> "Error found!" + """ + defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do + domain = expand_domain(domain, __CALLER__) msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__) msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__) @@ -76,6 +151,23 @@ defmodule Gettext.Macros do {msgid, msgid_plural} end + @doc """ + Marks the given message for extraction and returns + `{msgid, msgid_plural}`. + + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value of this macro is `{msgid, + msgid_plural}`. + + ## Examples + + my_fun = fn {msgid, msgid_plural} -> + # do something with msgid and msgid_plural + end + + my_fun.(dngettext_noop("errors", "One error", "%{count} errors")) + + """ defmacro dngettext_noop(domain, msgid, msgid_plural) do quote do unquote(__MODULE__).dpngettext_noop( @@ -87,6 +179,19 @@ defmodule Gettext.Macros do end end + @doc """ + Marks the given message for extraction and returns it unchanged. + + This macro can be used to mark a message for extraction when `mix + gettext.extract` is run. The return value is the given string, so that this + macro can be used seamlessly in place of the string to extract. + + ## Examples + + pngettext_noop("Home page", "Error found!", "Errors found!") + #=> "Error found!" + + """ defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do quote do unquote(__MODULE__).dpngettext_noop( @@ -98,6 +203,10 @@ defmodule Gettext.Macros do end end + @doc """ + Same as `dngettext_noop("default", msgid, mgsid_plural)`, but will use a + per-backend configured default domain if provided. + """ defmacro ngettext_noop(msgid, msgid_plural) do quote do unquote(__MODULE__).dpngettext_noop( @@ -109,6 +218,13 @@ defmodule Gettext.Macros do end end + @doc """ + Translates the given `msgid` with a given context (`msgctxt`) in the given `domain`. + + `bindings` is a map of bindings to support interpolation. + + See also `Gettext.dpgettext/5`. + """ defmacro dpgettext(domain, msgctxt, msgid, bindings \\ Macro.escape(%{})) do domain = expand_domain(domain, __CALLER__) @@ -126,12 +242,26 @@ defmodule Gettext.Macros do end end + @doc """ + Translates the given `msgid` in the given `domain`. + + `bindings` is a map of bindings to support interpolation. + + See also `Gettext.dgettext/4`. + """ defmacro dgettext(domain, msgid, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings)) end end + @doc """ + Translates the given `msgid` with the given context (`msgctxt`). + + `bindings` is a map of bindings to support interpolation. + + See also `Gettext.pgettext/4`. + """ defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpgettext( @@ -143,6 +273,12 @@ defmodule Gettext.Macros do end end + @doc """ + Same as `dgettext("default", msgid, %{})`, but will use a per-backend + configured default domain if provided. + + See also `Gettext.gettext/3`. + """ defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpgettext( @@ -154,6 +290,15 @@ defmodule Gettext.Macros do end end + @doc """ + Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`) + in the given `domain`. + + `n` is an integer used to determine how to pluralize the + message. `bindings` is a map of bindings to support interpolation. + + See also `Gettext.dpngettext/7`. + """ defmacro dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do domain = expand_domain(domain, __CALLER__) @@ -178,6 +323,15 @@ defmodule Gettext.Macros do end end + @doc """ + Translates the given plural message (`msgid` + `msgid_plural`) in the + given `domain`. + + `n` is an integer used to determine how to pluralize the + message. `bindings` is a map of bindings to support interpolation. + + See also `Gettext.dngettext/6`. + """ defmacro dngettext(domain, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpngettext( @@ -191,6 +345,12 @@ defmodule Gettext.Macros do end end + @doc """ + Same as `dngettext("default", msgid, msgid_plural, n, bindings)`, but will + use a per-backend configured default domain if provided. + + See also `Gettext.ngettext/5`. + """ defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpngettext( @@ -204,6 +364,14 @@ defmodule Gettext.Macros do end end + @doc """ + Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`). + + `n` is an integer used to determine how to pluralize the + message. `bindings` is a map of bindings to support interpolation. + + See also `Gettext.pngettext/6`. + """ defmacro pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpngettext( @@ -217,6 +385,28 @@ defmodule Gettext.Macros do end end + @doc """ + Stores an "extracted comment" for the next message. + + This macro can be used to add comments (Gettext refers to such + comments as *extracted comments*) to the next message that will + be extracted. Extracted comments will be prefixed with `#.` in POT + files. + + Calling this function multiple times will accumulate the comments; + when another Gettext macro (such as `gettext/2`) is called, + the comments will be extracted and attached to that message, and + they will be flushed so as to start again. + + This macro always returns `:ok`. + + ## Examples + + gettext_comment("The next message is awesome") + gettext_comment("Another comment for the next message") + gettext("The awesome message") + + """ defmacro gettext_comment(comment) do comment = Gettext.Compiler.expand_to_binary(comment, "comment", __MODULE__, __CALLER__) Gettext.Compiler.append_extracted_comment(comment) @@ -228,7 +418,7 @@ defmodule Gettext.Macros do Macro.expand(backend, env).__gettext__(:default_domain) else quote do - unquote(backend).__gettext__(:default_domain) + apply(unquote(backend), :__gettext__, [:default_domain]) end end end From 2810c37e8e880dc91dcf26f143374c41f8c1c98c Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sun, 18 Aug 2024 10:38:39 +0200 Subject: [PATCH 3/3] More more more --- lib/gettext/extractor.ex | 15 ++++++++++- lib/gettext/macros.ex | 56 +++++++++++++--------------------------- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/lib/gettext/extractor.ex b/lib/gettext/extractor.ex index c6a1d4b..c19b6a7 100644 --- a/lib/gettext/extractor.ex +++ b/lib/gettext/extractor.ex @@ -68,10 +68,23 @@ defmodule Gettext.Extractor do Note that this function doesn't perform any operation on the filesystem. """ - @spec extract(Macro.Env.t(), module, binary, binary, binary | {binary, binary}, [binary]) :: :ok + @spec extract( + Macro.Env.t(), + backend :: module, + domain :: binary | :default, + msgctxt :: binary, + id :: binary | {binary, binary}, + extracted_comments :: [binary] + ) :: :ok def extract(%Macro.Env{} = caller, backend, domain, msgctxt, id, extracted_comments) do format_flag = backend.__gettext__(:interpolation).message_format() + domain = + case domain do + :default -> backend.__gettext__(:default_domain) + string when is_binary(string) -> string + end + message = create_message_struct( id, diff --git a/lib/gettext/macros.ex b/lib/gettext/macros.ex index 2e34bdc..12ad45c 100644 --- a/lib/gettext/macros.ex +++ b/lib/gettext/macros.ex @@ -19,6 +19,10 @@ defmodule Gettext.Macros do @moduledoc since: "0.26.0" + @doc false + def __expand_runtime_domain__(backend, :default), do: backend.__gettext__(:default_domain) + def __expand_runtime_domain__(_backend, domain) when is_binary(domain), do: domain + @doc """ Marks the given message for extraction and returns it unchanged. @@ -85,11 +89,7 @@ defmodule Gettext.Macros do """ defmacro gettext_noop(msgid) do quote do - unquote(__MODULE__).dpgettext_noop( - {@__gettext_backend__, :__default_domain__}, - nil, - unquote(msgid) - ) + unquote(__MODULE__).dpgettext_noop(:default, nil, unquote(msgid)) end end @@ -108,11 +108,7 @@ defmodule Gettext.Macros do """ defmacro pgettext_noop(msgid, context) do quote do - unquote(__MODULE__).dpgettext_noop( - {@__gettext_backend__, :__default_domain__}, - unquote(context), - unquote(msgid) - ) + unquote(__MODULE__).dpgettext_noop(:default, unquote(context), unquote(msgid)) end end @@ -195,7 +191,7 @@ defmodule Gettext.Macros do defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do quote do unquote(__MODULE__).dpngettext_noop( - {@__gettext_backend__, :__default_domain__}, + :default, unquote(msgctxt), unquote(msgid), unquote(msgid_plural) @@ -209,12 +205,7 @@ defmodule Gettext.Macros do """ defmacro ngettext_noop(msgid, msgid_plural) do quote do - unquote(__MODULE__).dpngettext_noop( - {@__gettext_backend__, :__default_domain__}, - nil, - unquote(msgid), - unquote(msgid_plural) - ) + unquote(__MODULE__).dpngettext_noop(:default, nil, unquote(msgid), unquote(msgid_plural)) end end @@ -234,7 +225,7 @@ defmodule Gettext.Macros do Gettext.dpgettext( @__gettext_backend__, - unquote(domain), + unquote(__MODULE__).__expand_runtime_domain__(@__gettext_backend__, unquote(domain)), unquote(msgctxt), msgid, unquote(bindings) @@ -265,7 +256,7 @@ defmodule Gettext.Macros do defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpgettext( - {@__gettext_backend__, :__default_domain__}, + :default, unquote(msgctxt), unquote(msgid), unquote(bindings) @@ -281,12 +272,7 @@ defmodule Gettext.Macros do """ defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do quote do - unquote(__MODULE__).dpgettext( - {@__gettext_backend__, :__default_domain__}, - nil, - unquote(msgid), - unquote(bindings) - ) + unquote(__MODULE__).dpgettext(:default, nil, unquote(msgid), unquote(bindings)) end end @@ -313,7 +299,7 @@ defmodule Gettext.Macros do Gettext.dpngettext( @__gettext_backend__, - unquote(domain), + unquote(__MODULE__).__expand_runtime_domain__(@__gettext_backend__, unquote(domain)), unquote(msgctxt), msgid, msgid_plural, @@ -354,7 +340,7 @@ defmodule Gettext.Macros do defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpngettext( - {@__gettext_backend__, :__default_domain__}, + :default, nil, unquote(msgid), unquote(msgid_plural), @@ -375,7 +361,7 @@ defmodule Gettext.Macros do defmacro pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do quote do unquote(__MODULE__).dpngettext( - {@__gettext_backend__, :__default_domain__}, + :default, unquote(msgctxt), unquote(msgid), unquote(msgid_plural), @@ -413,17 +399,11 @@ defmodule Gettext.Macros do :ok end - defp expand_domain({backend, :__default_domain__}, env) do - if Gettext.Extractor.extracting?() do - Macro.expand(backend, env).__gettext__(:default_domain) - else - quote do - apply(unquote(backend), :__gettext__, [:default_domain]) - end - end + defp expand_domain(:default, _env) do + :default end - defp expand_domain(domain, _env) do - domain + defp expand_domain(domain, env) do + Gettext.Compiler.expand_to_binary(domain, "domain", __MODULE__, env) end end