diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe579267..7a1299f7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,9 +17,9 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-22.04 - erlang: "27.2" - elixir: "1.18" + - os: ubuntu-24.04 + erlang: "29.0" + elixir: "1.20" lint: true coverage: true @@ -31,7 +31,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install OTP and Elixir uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 diff --git a/.github/workflows/publish-to-hex.yml b/.github/workflows/publish-to-hex.yml index 791f63dd..cc41e269 100644 --- a/.github/workflows/publish-to-hex.yml +++ b/.github/workflows/publish-to-hex.yml @@ -16,13 +16,13 @@ jobs: steps: - name: Checkout this repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install Erlang and Elixir uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 with: - otp-version: "27.2" - elixir-version: "1.18" + otp-version: "29.0" + elixir-version: "1.20" - name: Fetch dependencies run: mix deps.get diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d38080..6cd9c7c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + + * Parallelize per-locale merging in `mix gettext.merge`. + ## v1.0.2 * Only skip manifest removal on Elixir v1.19.3+ diff --git a/lib/mix/tasks/gettext.merge.ex b/lib/mix/tasks/gettext.merge.ex index 49efebc4..3371bbab 100644 --- a/lib/mix/tasks/gettext.merge.ex +++ b/lib/mix/tasks/gettext.merge.ex @@ -191,9 +191,17 @@ defmodule Mix.Tasks.Gettext.Merge do end defp merge_all_locale_dirs(pot_dir, opts, gettext_config) do - for locale <- File.ls!(pot_dir), File.dir?(Path.join(pot_dir, locale)) do - merge_dirs(locale_dir(pot_dir, locale), pot_dir, locale, opts, gettext_config) - end + pot_dir + |> File.ls!() + |> Enum.filter(&File.dir?(Path.join(pot_dir, &1))) + |> Task.async_stream( + fn locale -> + merge_dirs(locale_dir(pot_dir, locale), pot_dir, locale, opts, gettext_config) + end, + ordered: false, + timeout: :infinity + ) + |> Stream.run() end def locale_dir(pot_dir, locale) do diff --git a/mix.lock b/mix.lock index 84b78a81..7e7415a3 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,10 @@ %{ - "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, + "castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, - "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, + "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, diff --git a/test/gettext/extractor_test.exs b/test/gettext/extractor_test.exs index cac32e50..80f45ab6 100644 --- a/test/gettext/extractor_test.exs +++ b/test/gettext/extractor_test.exs @@ -461,13 +461,16 @@ defmodule Gettext.ExtractorTest do end """ + # No trailing newline in the expected string below: the logged message ends + # without one, so when color is enabled the reset code (\e[0m) sits between + # the message and Logger's newline. Anchoring on "\n" would fail in a TTY. assert capture_log(fn -> Code.compile_string(code, Path.join(File.cwd!(), "foo.ex")) end) =~ """ Plural message for 'one error' is not matching: Using 'multiple errors' instead of '%{count} errors'. - References: foo.ex:9, foo.ex:10 + References: foo.ex:9, foo.ex:10\ """ after Extractor.disable() diff --git a/test/gettext/interpolation/default_test.exs b/test/gettext/interpolation/default_test.exs index 94ce9109..e2fc766c 100644 --- a/test/gettext/interpolation/default_test.exs +++ b/test/gettext/interpolation/default_test.exs @@ -107,8 +107,12 @@ defmodule Gettext.Interpolation.DefaultTest do ) end + # Building the empty bindings via `Map.drop/2` (rather than a `%{}` + # literal) keeps the compiler's type checker from inferring that + # `translate` only accepts maps with a `:count` key, which would flag + # this intentional missing-key case. assert_raise MatchError, fn -> - translate.(%{}) + translate.(Map.drop(%{count: 7}, [:count])) end assert {:ok, "7 shoes"} = translate.(%{count: 7}) diff --git a/test/gettext/merger_test.exs b/test/gettext/merger_test.exs index 916791be..7db679dd 100644 --- a/test/gettext/merger_test.exs +++ b/test/gettext/merger_test.exs @@ -67,8 +67,7 @@ defmodule Gettext.MergerTest do %Message.Singular{msgid: "obs_auto", obsolete: true}, %Message.Singular{msgid: "obs_manual", obsolete: true} ] - }, - stats} = + }, stats} = Merger.merge( old_po, new_pot, diff --git a/test/mix/tasks/gettext.extract_test.exs b/test/mix/tasks/gettext.extract_test.exs index 97957c66..8a1c8b71 100644 --- a/test/mix/tasks/gettext.extract_test.exs +++ b/test/mix/tasks/gettext.extract_test.exs @@ -14,14 +14,15 @@ defmodule Mix.Tasks.Gettext.ExtractTest do test "extracting and extracting with --merge", %{test: test, tmp_dir: tmp_dir} = context do create_test_mix_file(context) + mod = unique_module(test) write_file(context, "lib/my_app.ex", """ - defmodule MyApp.Gettext do + defmodule #{mod}.Gettext do use Gettext.Backend, otp_app: #{inspect(test)} end - defmodule MyApp do - use Gettext, backend: MyApp.Gettext + defmodule #{mod} do + use Gettext, backend: #{mod}.Gettext def foo(), do: gettext("hello") end """) @@ -43,8 +44,8 @@ defmodule Mix.Tasks.Gettext.ExtractTest do # Test --merge too. write_file(context, "lib/other.ex", """ - defmodule MyApp.Other do - use Gettext, backend: MyApp.Gettext + defmodule #{mod}.Other do + use Gettext, backend: #{mod}.Gettext def foo(), do: dgettext("my_domain", "other") end """) @@ -70,21 +71,22 @@ defmodule Mix.Tasks.Gettext.ExtractTest do test "--check-up-to-date should fail if no POT files have been created", %{test: test, tmp_dir: tmp_dir} = context do create_test_mix_file(context) + mod = unique_module(test) write_file(context, "lib/my_app.ex", """ - defmodule MyApp.Gettext do + defmodule #{mod}.Gettext do use Gettext.Backend, otp_app: #{inspect(test)} end - defmodule MyApp do - use Gettext, backend: MyApp.Gettext + defmodule #{mod} do + use Gettext, backend: #{mod}.Gettext def foo(), do: gettext("hello") end """) write_file(context, "lib/other.ex", """ - defmodule MyApp.Other do - use Gettext, backend: MyApp.Gettext + defmodule #{mod}.Other do + use Gettext, backend: #{mod}.Gettext def foo(), do: dgettext("my_domain", "other") end """) @@ -109,14 +111,15 @@ defmodule Mix.Tasks.Gettext.ExtractTest do test "--check-up-to-date should pass if nothing changed", %{test: test, tmp_dir: tmp_dir} = context do create_test_mix_file(context, write_reference_comments: false) + mod = unique_module(test) write_file(context, "lib/my_app.ex", """ - defmodule MyApp.Gettext do + defmodule #{mod}.Gettext do use Gettext.Backend, otp_app: #{inspect(test)} end - defmodule MyApp do - use Gettext, backend: MyApp.Gettext + defmodule #{mod} do + use Gettext, backend: #{mod}.Gettext def foo(), do: gettext("hello") end """) @@ -135,21 +138,22 @@ defmodule Mix.Tasks.Gettext.ExtractTest do test "--check-up-to-date should fail if POT files are outdated", %{test: test, tmp_dir: tmp_dir} = context do create_test_mix_file(context) + mod = unique_module(test) write_file(context, "lib/my_app.ex", """ - defmodule MyApp.Gettext do + defmodule #{mod}.Gettext do use Gettext.Backend, otp_app: #{inspect(test)} end - defmodule MyApp do - use Gettext, backend: MyApp.Gettext + defmodule #{mod} do + use Gettext, backend: #{mod}.Gettext def foo(), do: gettext("hello") end """) write_file(context, "lib/other.ex", """ - defmodule MyApp.Other do - use Gettext, backend: MyApp.Gettext + defmodule #{mod}.Other do + use Gettext, backend: #{mod}.Gettext def foo(), do: dgettext("my_domain", "other") end """) @@ -159,12 +163,12 @@ defmodule Mix.Tasks.Gettext.ExtractTest do end) write_file(context, "lib/my_app.ex", """ - defmodule MyApp.Gettext do + defmodule #{mod}.Gettext do use Gettext.Backend, otp_app: #{inspect(test)} end - defmodule MyApp do - use Gettext, backend: MyApp.Gettext + defmodule #{mod} do + use Gettext, backend: #{mod}.Gettext def foo(), do: gettext("hello world") end """) diff --git a/test/support/mix_project_helpers.ex b/test/support/mix_project_helpers.ex index 4e6c427f..7cb11bde 100644 --- a/test/support/mix_project_helpers.ex +++ b/test/support/mix_project_helpers.ex @@ -1,4 +1,14 @@ defmodule GettextTest.MixProjectHelpers do + # Returns a module name segment that is unique per test. Tests in this suite + # run in the same VM and reuse fixture module names; because + # `Mix.Project.in_project/4` is called with `prune_code_paths: false`, every + # test's compiled fixtures stay in the code path. Reusing a module name across + # tests would let an earlier test's stale beam shadow the freshly compiled one, + # so we derive a unique base module name from the test name instead. + def unique_module(test) do + "MyApp" <> Integer.to_string(:erlang.phash2(test)) + end + def create_test_mix_file(context, gettext_config \\ []) do write_file(context, "mix.exs", """ defmodule MyApp.MixProject do