From 7cf265ab8a241d4d49ce8a7174504a511b3bb517 Mon Sep 17 00:00:00 2001 From: Oliver Kriska Date: Thu, 15 Oct 2026 14:28:50 +0200 Subject: [PATCH] Compile the interpolation patterns once per VM to_interpolatable/1 compiled the "%{" and "}" patterns with :binary.compile_pattern/1 on every call. Each compilation costs about as much as splitting a short string, and this function runs on every runtime interpolation: whenever the current locale has no translation for a message, including every render in the default locale of an application without a PO file for it, and once per message at compile time. A compiled pattern is a reference, so it cannot live in a module attribute; the two patterns are now compiled on first use and kept in persistent_term. Passing the literal strings to :binary.split/2 instead would compile them again on every split and gets slower as the number of bindings grows. Microbenchmark of runtime_interpolate/2 (compiled loop, median of 7 alternating samples, Elixir 1.20.4 / OTP 29, one scheduler; OTP 27 within 5% of these): input before after no bindings 0.44 us 0.15 us 1 binary binding 0.72 us 0.39 us 3 binary bindings 1.06 us 0.78 us 32 bindings 7.76 us 7.27 us long text, 1 binding 0.75 us 0.45 us --- lib/gettext/interpolation/default.ex | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/gettext/interpolation/default.ex b/lib/gettext/interpolation/default.ex index 7dfd42b..604c726 100644 --- a/lib/gettext/interpolation/default.ex +++ b/lib/gettext/interpolation/default.ex @@ -22,14 +22,30 @@ defmodule Gettext.Interpolation.Default do @doc false @spec to_interpolatable(String.t()) :: interpolatable() def to_interpolatable(string) when is_binary(string) do - start_pattern = :binary.compile_pattern("%{") - end_pattern = :binary.compile_pattern("}") + {start_pattern, end_pattern} = patterns() string |> to_interpolatable(_current = "", _acc = [], start_pattern, end_pattern) |> Enum.reverse() end + # The patterns are constant, but a compiled pattern is a reference, so it + # cannot be a module attribute. Compiling them on every call costs more than + # the split itself for short strings, so they are compiled once per VM. + @patterns_key {__MODULE__, :patterns} + + defp patterns do + case :persistent_term.get(@patterns_key, nil) do + nil -> + patterns = {:binary.compile_pattern("%{"), :binary.compile_pattern("}")} + :persistent_term.put(@patterns_key, patterns) + patterns + + patterns -> + patterns + end + end + defp to_interpolatable(string, current, acc, start_pattern, end_pattern) do case :binary.split(string, start_pattern) do # If we have one element, no %{ was found so this is the final part of the