From c478cc89e6ecb394be1bee3597c9e7af43588f3e Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 2 Jun 2026 09:27:04 +0200 Subject: [PATCH 1/8] Bump CI to Elixir 1.19 and OTP 28.5 Update the primary test matrix entry and the Hex publish workflow to use Elixir 1.19 on Erlang/OTP 28.5, running on ubuntu-24.04. --- .github/workflows/main.yml | 6 +++--- .github/workflows/publish-to-hex.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe57926..799f416 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: "28.5" + elixir: "1.19" lint: true coverage: true diff --git a/.github/workflows/publish-to-hex.yml b/.github/workflows/publish-to-hex.yml index 791f63d..9820ed0 100644 --- a/.github/workflows/publish-to-hex.yml +++ b/.github/workflows/publish-to-hex.yml @@ -21,8 +21,8 @@ jobs: - name: Install Erlang and Elixir uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 with: - otp-version: "27.2" - elixir-version: "1.18" + otp-version: "28.5" + elixir-version: "1.19" - name: Fetch dependencies run: mix deps.get From dda4d5086fcff0cdc986cecdaddca3c17efe5b78 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 2 Jun 2026 09:28:51 +0200 Subject: [PATCH 2/8] Reformat for Elixir 1.19 formatter --- test/gettext/merger_test.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/gettext/merger_test.exs b/test/gettext/merger_test.exs index 916791b..7db679d 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, From 640d55e4a56f7f1c12dd37b4e66e334273c11c75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:51:37 +0200 Subject: [PATCH 3/8] Bump actions/checkout from 6.0.2 to 6.0.3 (#435) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- .github/workflows/publish-to-hex.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 799f416..486b344 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 9820ed0..2903034 100644 --- a/.github/workflows/publish-to-hex.yml +++ b/.github/workflows/publish-to-hex.yml @@ -16,7 +16,7 @@ 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 From 8f78b58098ed4bb63ea9f7ad2ccefb2a5a6c4514 Mon Sep 17 00:00:00 2001 From: Oliver Kriska Date: Wed, 10 Jun 2026 16:21:01 +0200 Subject: [PATCH 4/8] Parallelize locale merging in mix gettext.merge (#436) Benchmark on a 16-locale / 11-domain Phoenix app (160 PO files, ~4,700 msgids, Elixir 1.20.1 / OTP 29, Apple Silicon): mix gettext.merge priv/gettext --no-fuzzy before: 9.4s wall (55% CPU) after: 1.9-2.4s wall (341-381% CPU) ~4.9x speedup with byte-identical output files (clean git status over committed PO state after both runs). The win scales with locale count. --- CHANGELOG.md | 4 ++++ lib/mix/tasks/gettext.merge.ex | 14 +++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d3808..6cd9c7c 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 49efebc..3371bba 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 From 2d1f7c974cbff0712b5696e18c8afcf0cecc9950 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 30 Jun 2026 09:40:26 +0200 Subject: [PATCH 5/8] Update local deps --- mix.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.lock b/mix.lock index 84b78a8..7e7415a 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"}, From 7d3512dcc52e085ff391c28f9fa667afbf8843b0 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 30 Jun 2026 09:40:31 +0200 Subject: [PATCH 6/8] Fix warnings in tests --- test/gettext/extractor_test.exs | 5 ++- test/gettext/interpolation/default_test.exs | 6 ++- test/mix/tasks/gettext.extract_test.exs | 46 +++++++++++---------- test/support/mix_project_helpers.ex | 10 +++++ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/test/gettext/extractor_test.exs b/test/gettext/extractor_test.exs index cac32e5..364946f 100644 --- a/test/gettext/extractor_test.exs +++ b/test/gettext/extractor_test.exs @@ -464,10 +464,13 @@ defmodule Gettext.ExtractorTest do assert capture_log(fn -> Code.compile_string(code, Path.join(File.cwd!(), "foo.ex")) end) =~ + # No trailing newline: 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. """ 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 94ce910..e2fc766 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/mix/tasks/gettext.extract_test.exs b/test/mix/tasks/gettext.extract_test.exs index 97957c6..8a1c8b7 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 4e6c427..7cb11bd 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 From 8636f04712bef624f5f798e9d1fb1df4c75d247f Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 30 Jun 2026 09:43:09 +0200 Subject: [PATCH 7/8] Format extractor_test comment to satisfy mix format The comment explaining the ANSI-color handling sat between `=~` and its heredoc argument, which `mix format` (1.19) reflows. Move it above the assertion so the format check passes under the modernized CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/gettext/extractor_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/gettext/extractor_test.exs b/test/gettext/extractor_test.exs index 364946f..80f45ab 100644 --- a/test/gettext/extractor_test.exs +++ b/test/gettext/extractor_test.exs @@ -461,12 +461,12 @@ 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) =~ - # No trailing newline: 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. """ Plural message for 'one error' is not matching: Using 'multiple errors' instead of '%{count} errors'. From 7108740ea93ca0e22914640ca65ba72dfdf8fe8b Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 30 Jun 2026 09:48:44 +0200 Subject: [PATCH 8/8] Bump CI to Elixir 1.20 and OTP 29 Move the lint/coverage matrix row and the hex publish workflow up to the latest Elixir (1.20) and Erlang/OTP (29) releases. The minimum-supported row (1.16/24.2) is left in place to keep testing the floor declared in mix.exs (~> 1.16). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/main.yml | 4 ++-- .github/workflows/publish-to-hex.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 486b344..7a1299f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,8 +18,8 @@ jobs: matrix: include: - os: ubuntu-24.04 - erlang: "28.5" - elixir: "1.19" + erlang: "29.0" + elixir: "1.20" lint: true coverage: true diff --git a/.github/workflows/publish-to-hex.yml b/.github/workflows/publish-to-hex.yml index 2903034..cc41e26 100644 --- a/.github/workflows/publish-to-hex.yml +++ b/.github/workflows/publish-to-hex.yml @@ -21,8 +21,8 @@ jobs: - name: Install Erlang and Elixir uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 with: - otp-version: "28.5" - elixir-version: "1.19" + otp-version: "29.0" + elixir-version: "1.20" - name: Fetch dependencies run: mix deps.get