Skip to content

Parallelize locale merging in mix gettext.merge#436

Merged
josevalim merged 1 commit into
elixir-gettext:mainfrom
oliver-kriska:parallel-locale-merge
Jun 10, 2026
Merged

Parallelize locale merging in mix gettext.merge#436
josevalim merged 1 commit into
elixir-gettext:mainfrom
oliver-kriska:parallel-locale-merge

Conversation

@oliver-kriska

Copy link
Copy Markdown
Contributor

Hi,
this is not huge change and it can help. Following text is generated by Claude Code based on my tests.

mix gettext.merge processes locales sequentially, but each locale's merge is fully independent: every locale task writes only to its own <pot_dir>/<locale>/LC_MESSAGES/*.po files, the POT files at the root of pot_dir are only read, and Gettext.Merger is stateless. Switch merge_all_locale_dirs/3 to Task.async_stream over locales, mirroring the existing Task.async_stream over POT files within a locale in merge_dirs/5.

The return value change (list -> :ok) is safe because the only caller is merge_messages_dir/3, whose result is discarded in run. The --locale single-locale path is untouched. A crashed locale task raises out of Stream.run/1, matching the sequential version's failure behavior. Mix.shell() output is GenServer-backed, so concurrent lines serialize correctly. The only observable change is that progress lines from different locales can interleave, as already happens in Gettext.Compiler's parallel paths.

Benchmark on a 16-locale / 11-domain Phoenix app (160 PO files, ~4700 msgids, Elixir 1.20.1 / OTP 29, M4 Pro):

    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.

mix gettext.merge processes locales sequentially, but each locale's
merge is fully independent: every locale task writes only to its own
<pot_dir>/<locale>/LC_MESSAGES/*.po files, the POT files at the root of
pot_dir are only read, and Gettext.Merger is stateless (pure functions
over Expo structs). Switch merge_all_locale_dirs/3 to Task.async_stream
over locales, mirroring the existing Task.async_stream over POT files
within a locale in merge_dirs/5.

The return value change (list -> :ok via Stream.run/1) is safe: the
only caller is merge_messages_dir/3, whose result is discarded in
run/1. The --locale single-locale path is untouched. A crashed locale
task raises out of Stream.run/1, matching the sequential version's
failure behavior. Mix.shell() output is GenServer-backed, so concurrent
lines serialize correctly; the only observable change is that progress
lines from different locales can interleave, as already happens in
Gettext.Compiler's parallel paths.

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.
@josevalim josevalim merged commit 3cf0065 into elixir-gettext:main Jun 10, 2026
2 checks passed
@josevalim

Copy link
Copy Markdown
Contributor

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants