Few untranslated error messages in OAuth

Started by Zhijie Hou (Fujitsu)2 months ago13 messages
#1Zhijie Hou (Fujitsu)
houzj.fnst@fujitsu.com
1 attachment(s)

Hi,

During testing of libpq, I noticed several error messages not processed by
libpq_gettext, resulting in their exclusion from *.pot files. The attached patch
wraps these messages.

Best Regards,
Hou zj

Attachments:

v1-0001-Fix-some-untranslatable-error-messages-in-OAuth.patchapplication/octet-stream; name=v1-0001-Fix-some-untranslatable-error-messages-in-OAuth.patchDownload
From 17918342432a15285dc35b24d2c9e080b9d0b818 Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Thu, 13 Nov 2025 13:32:02 +0800
Subject: [PATCH v1] Fix some untranslatable error messages in OAuth

---
 src/interfaces/libpq-oauth/oauth-curl.c | 28 ++++++++++++-------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 08e300715da..515f4d9b4eb 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -475,20 +475,20 @@ report_type_mismatch(struct oauth_parse *ctx)
 	switch (ctx->active->type)
 	{
 		case JSON_TOKEN_STRING:
-			msgfmt = "field \"%s\" must be a string";
+			msgfmt = libpq_gettext("field \"%s\" must be a string");
 			break;
 
 		case JSON_TOKEN_NUMBER:
-			msgfmt = "field \"%s\" must be a number";
+			msgfmt = libpq_gettext("field \"%s\" must be a number");
 			break;
 
 		case JSON_TOKEN_ARRAY_START:
-			msgfmt = "field \"%s\" must be an array of strings";
+			msgfmt = libpq_gettext("field \"%s\" must be an array of strings");
 			break;
 
 		default:
 			Assert(false);
-			msgfmt = "field \"%s\" has unexpected type";
+			msgfmt = libpq_gettext("field \"%s\" has unexpected type");
 	}
 
 	oauth_parse_set_error(ctx, msgfmt, ctx->active->name);
@@ -1087,7 +1087,7 @@ parse_token_error(struct async_ctx *actx, struct token_error *err)
 	 * override the errctx if parsing explicitly fails.
 	 */
 	if (!result)
-		actx->errctx = "failed to parse token error response";
+		actx->errctx = libpq_gettext("failed to parse token error response");
 
 	return result;
 }
@@ -2153,7 +2153,7 @@ finish_discovery(struct async_ctx *actx)
 	/*
 	 * Pull the fields we care about from the document.
 	 */
-	actx->errctx = "failed to parse OpenID discovery document";
+	actx->errctx = libpq_gettext("failed to parse OpenID discovery document");
 	if (!parse_provider(actx, &actx->provider))
 		return false;			/* error message already set */
 
@@ -2421,7 +2421,7 @@ finish_device_authz(struct async_ctx *actx)
 	 */
 	if (response_code == 200)
 	{
-		actx->errctx = "failed to parse device authorization";
+		actx->errctx = libpq_gettext("failed to parse device authorization");
 		if (!parse_device_authz(actx, &actx->authz))
 			return false;		/* error message already set */
 
@@ -2509,7 +2509,7 @@ finish_token_request(struct async_ctx *actx, struct token *tok)
 	 */
 	if (response_code == 200)
 	{
-		actx->errctx = "failed to parse access token response";
+		actx->errctx = libpq_gettext("failed to parse access token response");
 		if (!parse_access_token(actx, tok))
 			return false;		/* error message already set */
 
@@ -2888,7 +2888,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 		switch (actx->step)
 		{
 			case OAUTH_STEP_INIT:
-				actx->errctx = "failed to fetch OpenID discovery document";
+				actx->errctx = libpq_gettext("failed to fetch OpenID discovery document");
 				if (!start_discovery(actx, conn_oauth_discovery_uri(conn)))
 					goto error_return;
 
@@ -2902,11 +2902,11 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				if (!check_issuer(actx, conn))
 					goto error_return;
 
-				actx->errctx = "cannot run OAuth device authorization";
+				actx->errctx = libpq_gettext("cannot run OAuth device authorization");
 				if (!check_for_device_flow(actx))
 					goto error_return;
 
-				actx->errctx = "failed to obtain device authorization";
+				actx->errctx = libpq_gettext("failed to obtain device authorization");
 				if (!start_device_authz(actx, conn))
 					goto error_return;
 
@@ -2917,7 +2917,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				if (!finish_device_authz(actx))
 					goto error_return;
 
-				actx->errctx = "failed to obtain access token";
+				actx->errctx = libpq_gettext("failed to obtain access token");
 				if (!start_token_request(actx, conn))
 					goto error_return;
 
@@ -2968,7 +2968,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				break;
 
 			case OAUTH_STEP_WAIT_INTERVAL:
-				actx->errctx = "failed to obtain access token";
+				actx->errctx = libpq_gettext("failed to obtain access token");
 				if (!start_token_request(actx, conn))
 					goto error_return;
 
@@ -2994,7 +2994,7 @@ error_return:
 	 * also the documentation for struct async_ctx.
 	 */
 	if (actx->errctx)
-		appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx));
+		appendPQExpBuffer(errbuf, "%s: ", actx->errctx);
 
 	if (PQExpBufferDataBroken(actx->errbuf))
 		appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
-- 
2.51.1.windows.1

#2Michael Paquier
michael@paquier.xyz
In reply to: Zhijie Hou (Fujitsu) (#1)
Re: Few untranslated error messages in OAuth

On Thu, Nov 13, 2025 at 06:01:29AM +0000, Zhijie Hou (Fujitsu) wrote:

During testing of libpq, I noticed several error messages not processed by
libpq_gettext, resulting in their exclusion from *.pot files. The attached patch
wraps these messages.

(Added Jacob and Daniel in CC.)

async_ctx says that errcxt "will be translated for you.", but these
are missing from the translation files. So yes, it looks like you are
right.
--
Michael

#3Álvaro Herrera
alvherre@kurilemu.de
In reply to: Zhijie Hou (Fujitsu) (#1)
Re: Few untranslated error messages in OAuth

On 2025-Nov-13, Zhijie Hou (Fujitsu) wrote:

Hi,

During testing of libpq, I noticed several error messages not processed by
libpq_gettext, resulting in their exclusion from *.pot files. The attached patch
wraps these messages.

Yeah, this roughly makes sense. I think the documentation for errctx in
async_ctx needs to be updated though, because currently it says "it'll
be translated for you" but this patch makes it the other way around (it
must come translated).

Alternatively, we could mark the strings with gettext_noop(), which only
adds the messages to the catalog without translating them; so the
responsibility of translating them would remain where it is today. This
might be easier.

--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"No tengo por qué estar de acuerdo con lo que pienso"
(Carlos Caszeli)

#4Daniel Gustafsson
daniel@yesql.se
In reply to: Álvaro Herrera (#3)
Re: Few untranslated error messages in OAuth

On 13 Nov 2025, at 09:13, Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-13, Zhijie Hou (Fujitsu) wrote:

During testing of libpq, I noticed several error messages not processed by
libpq_gettext, resulting in their exclusion from *.pot files. The attached patch
wraps these messages.

Thanks for the report!

Yeah, this roughly makes sense. I think the documentation for errctx in
async_ctx needs to be updated though, because currently it says "it'll
be translated for you" but this patch makes it the other way around (it
must come translated).

+1

Alternatively, we could mark the strings with gettext_noop(), which only
adds the messages to the catalog without translating them; so the
responsibility of translating them would remain where it is today. This
might be easier.

I was pondering that since auth error messages in backend libpq does this, but
since we don't use gettext_noop anywhere else in frontend libpq today it makes
sense to go with libpq_gettext and update the docs instead.

Unless you, who has more translation/nls insights than me, feel strongly about
gettext_noop I'll go ahead with the current patch.

--
Daniel Gustafsson

#5Álvaro Herrera
alvherre@kurilemu.de
In reply to: Daniel Gustafsson (#4)
Re: Few untranslated error messages in OAuth

On 2025-Nov-13, Daniel Gustafsson wrote:

I was pondering that since auth error messages in backend libpq does this, but
since we don't use gettext_noop anywhere else in frontend libpq today it makes
sense to go with libpq_gettext and update the docs instead.

Unless you, who has more translation/nls insights than me, feel strongly about
gettext_noop I'll go ahead with the current patch.

gettext_noop() does something completely different from libpq_gettext(),
and the more I think about this, the more I think doing libpq_gettext()
is the wrong thing. I can give it a shot if you want.

--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Update: super-fast reaction on the Postgres bugs mailing list. The report
was acknowledged [...], and a fix is under discussion.
The wonders of open-source !"
https://twitter.com/gunnarmorling/status/1596080409259003906

#6Daniel Gustafsson
daniel@yesql.se
In reply to: Álvaro Herrera (#5)
Re: Few untranslated error messages in OAuth

On 13 Nov 2025, at 13:32, Álvaro Herrera <alvherre@kurilemu.de> wrote:

On 2025-Nov-13, Daniel Gustafsson wrote:

I was pondering that since auth error messages in backend libpq does this, but
since we don't use gettext_noop anywhere else in frontend libpq today it makes
sense to go with libpq_gettext and update the docs instead.

Unless you, who has more translation/nls insights than me, feel strongly about
gettext_noop I'll go ahead with the current patch.

gettext_noop() does something completely different from libpq_gettext(),
and the more I think about this, the more I think doing libpq_gettext()
is the wrong thing.

If so, should that be extended to oauth_json_set_error as well? (and possibly others?)

I can give it a shot if you want.

If you have time, that would be great.

--
Daniel Gustafsson

#7Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Daniel Gustafsson (#6)
Re: Few untranslated error messages in OAuth

On Thu, Nov 13, 2025 at 4:49 AM Daniel Gustafsson <daniel@yesql.se> wrote:

If so, should that be extended to oauth_json_set_error as well? (and possibly others?)

That should be covered already by nls.mk, unless I messed that up
too... I'll do a deeper dive.

Another way to fix this would be to wrap the assignment to errctx in a
macro that nls.mk can key off of, like oauth_json_set_error() does.
But I'm curious what Alvaro has in mind?

--Jacob

#8Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#7)
Re: Few untranslated error messages in OAuth

On Thu, Nov 13, 2025 at 8:23 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

On Thu, Nov 13, 2025 at 4:49 AM Daniel Gustafsson <daniel@yesql.se> wrote:

If so, should that be extended to oauth_json_set_error as well? (and possibly others?)

That should be covered already by nls.mk, unless I messed that up
too... I'll do a deeper dive.

I misread your email, sorry. We trigger on oauth_parse_set_error(),
but not oauth_json_set_error(). I'll see how easy that is to change.

There's more to fix here, after talking with Alvaro offlist. I got
gettext to tell me [1]https://www.gnu.org/software/gettext/manual/html_node/Prioritizing-messages.html what else was missing and found that
- fe-auth-oauth.c is not in GETTEXT_FILES at all
- I didn't give jsonapi.c the same handling as in 3ddbac368 when I
ported it to libpq, so it's not translated either

I'm writing up a patch that combines all of that.

Thanks,
--Jacob

[1]: https://www.gnu.org/software/gettext/manual/html_node/Prioritizing-messages.html

#9Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#8)
4 attachment(s)
Re: Few untranslated error messages in OAuth

On Thu, Nov 13, 2025 at 3:08 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I'm writing up a patch that combines all of that.

And here's what I have so far. I could use help double-checking the
.po result; I'm not entirely sure my usage of update-po is correct.

I took this opportunity to address a complaint from Alvaro and Peter E
a little while back, that some of the more internal-facing error
messages are very difficult to translate (and wouldn't help users even
if translation were easy). I can pull that (v2-0002) into its own
thread if necessary.

- v2-0001: combines Zhijie Hou's patch with the gettext_noop()
suggestion from Alvaro and fixes the nls.mk omission
- v2-0002: removes translation of multiplexer failures by adding an
_internal macro
- v2-0003: aligns oauth_json_set_error() with the prior commits
- v2-0004: tries to get jsonapi.c translated too

Unfortunately v2-0004 doesn't work. It puts the messages into the
translation files, but we use the _() macro throughout jsonapi.c,
which isn't helpful for libpq because libpq uses its own text domain.
This was an oversight in the work done for 0785d1b8b, I think, and it
may need its own patchset unless someone has a really quick fix.

Please let me know if I forgot to address something already said
above. I assume translation changes such as these are generally
backportable?

Thanks,
--Jacob

Attachments:

v2-0001-libpq-Add-missing-OAuth-translations.patchapplication/x-patch; name=v2-0001-libpq-Add-missing-OAuth-translations.patchDownload
From 4a7e11a67f44c127bf00446141b9f4e73e9414a4 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 12:42:00 -0800
Subject: [PATCH v2 1/4] libpq: Add missing OAuth translations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Several strings that should have been translated as they passed through
libpq_gettext were not actually being pulled into the translation files,
because I hadn't directly wrapped them in one of the GETTEXT_TRIGGERS.

Move the responsibility for calling libpq_gettext() to the code that
sets actx->errctx. Doing the same in report_type_mismatch() would result
in double-translation, so mark those strings with gettext_noop()
instead. And wrap two ternary operands with gettext_noop(), even though
they're already in one of the triggers, since xgettext sees only the
first.

Finally, fe-auth-oauth.c was missing from nls.mk, so none of that file
was being translated at all. Add it now.

Original patch by Zhijie Hou, plus suggested tweaks by Álvaro Herrera
and small additions by me.

Reported-by: Zhijie Hou <houzj.fnst@fujitsu.com>
Author: Zhijie Hou <houzj.fnst@fujitsu.com>
Co-authored-by: Álvaro Herrera <alvherre@kurilemu.de>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/TY4PR01MB1690746DB91991D1E9A47F57E94CDA%40TY4PR01MB16907.jpnprd01.prod.outlook.com
Backpatch-through: ?
---
 src/interfaces/libpq-oauth/oauth-curl.c | 35 +++++++++++++------------
 src/interfaces/libpq/nls.mk             |  1 +
 2 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 08e300715da..8bd1870cf8a 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -247,7 +247,8 @@ struct async_ctx
 	 * our entry point, errors have three parts:
 	 *
 	 * - errctx:	an optional static string, describing the global operation
-	 *				currently in progress. It'll be translated for you.
+	 *				currently in progress. Should be translated with
+	 *				libpq_gettext().
 	 *
 	 * - errbuf:	contains the actual error message. Generally speaking, use
 	 *				actx_error[_str] to manipulate this. This must be filled
@@ -475,20 +476,20 @@ report_type_mismatch(struct oauth_parse *ctx)
 	switch (ctx->active->type)
 	{
 		case JSON_TOKEN_STRING:
-			msgfmt = "field \"%s\" must be a string";
+			msgfmt = gettext_noop("field \"%s\" must be a string");
 			break;
 
 		case JSON_TOKEN_NUMBER:
-			msgfmt = "field \"%s\" must be a number";
+			msgfmt = gettext_noop("field \"%s\" must be a number");
 			break;
 
 		case JSON_TOKEN_ARRAY_START:
-			msgfmt = "field \"%s\" must be an array of strings";
+			msgfmt = gettext_noop("field \"%s\" must be an array of strings");
 			break;
 
 		default:
 			Assert(false);
-			msgfmt = "field \"%s\" has unexpected type";
+			msgfmt = gettext_noop("field \"%s\" has unexpected type");
 	}
 
 	oauth_parse_set_error(ctx, msgfmt, ctx->active->name);
@@ -1087,7 +1088,7 @@ parse_token_error(struct async_ctx *actx, struct token_error *err)
 	 * override the errctx if parsing explicitly fails.
 	 */
 	if (!result)
-		actx->errctx = "failed to parse token error response";
+		actx->errctx = libpq_gettext("failed to parse token error response");
 
 	return result;
 }
@@ -1115,8 +1116,8 @@ record_token_error(struct async_ctx *actx, const struct token_error *err)
 		if (response_code == 401)
 		{
 			actx_error(actx, actx->used_basic_auth
-					   ? "provider rejected the oauth_client_secret"
-					   : "provider requires client authentication, and no oauth_client_secret is set");
+					   ? gettext_noop("provider rejected the oauth_client_secret")
+					   : gettext_noop("provider requires client authentication, and no oauth_client_secret is set"));
 			actx_error_str(actx, " ");
 		}
 	}
@@ -2153,7 +2154,7 @@ finish_discovery(struct async_ctx *actx)
 	/*
 	 * Pull the fields we care about from the document.
 	 */
-	actx->errctx = "failed to parse OpenID discovery document";
+	actx->errctx = libpq_gettext("failed to parse OpenID discovery document");
 	if (!parse_provider(actx, &actx->provider))
 		return false;			/* error message already set */
 
@@ -2421,7 +2422,7 @@ finish_device_authz(struct async_ctx *actx)
 	 */
 	if (response_code == 200)
 	{
-		actx->errctx = "failed to parse device authorization";
+		actx->errctx = libpq_gettext("failed to parse device authorization");
 		if (!parse_device_authz(actx, &actx->authz))
 			return false;		/* error message already set */
 
@@ -2509,7 +2510,7 @@ finish_token_request(struct async_ctx *actx, struct token *tok)
 	 */
 	if (response_code == 200)
 	{
-		actx->errctx = "failed to parse access token response";
+		actx->errctx = libpq_gettext("failed to parse access token response");
 		if (!parse_access_token(actx, tok))
 			return false;		/* error message already set */
 
@@ -2888,7 +2889,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 		switch (actx->step)
 		{
 			case OAUTH_STEP_INIT:
-				actx->errctx = "failed to fetch OpenID discovery document";
+				actx->errctx = libpq_gettext("failed to fetch OpenID discovery document");
 				if (!start_discovery(actx, conn_oauth_discovery_uri(conn)))
 					goto error_return;
 
@@ -2902,11 +2903,11 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				if (!check_issuer(actx, conn))
 					goto error_return;
 
-				actx->errctx = "cannot run OAuth device authorization";
+				actx->errctx = libpq_gettext("cannot run OAuth device authorization");
 				if (!check_for_device_flow(actx))
 					goto error_return;
 
-				actx->errctx = "failed to obtain device authorization";
+				actx->errctx = libpq_gettext("failed to obtain device authorization");
 				if (!start_device_authz(actx, conn))
 					goto error_return;
 
@@ -2917,7 +2918,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				if (!finish_device_authz(actx))
 					goto error_return;
 
-				actx->errctx = "failed to obtain access token";
+				actx->errctx = libpq_gettext("failed to obtain access token");
 				if (!start_token_request(actx, conn))
 					goto error_return;
 
@@ -2968,7 +2969,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				break;
 
 			case OAUTH_STEP_WAIT_INTERVAL:
-				actx->errctx = "failed to obtain access token";
+				actx->errctx = libpq_gettext("failed to obtain access token");
 				if (!start_token_request(actx, conn))
 					goto error_return;
 
@@ -2994,7 +2995,7 @@ error_return:
 	 * also the documentation for struct async_ctx.
 	 */
 	if (actx->errctx)
-		appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx));
+		appendPQExpBuffer(errbuf, "%s: ", actx->errctx);
 
 	if (PQExpBufferDataBroken(actx->errbuf))
 		appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index b87df277d93..111e4849ed5 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,7 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
 GETTEXT_FILES    = fe-auth.c \
+                   fe-auth-oauth.c \
                    fe-auth-scram.c \
                    fe-cancel.c \
                    fe-connect.c \
-- 
2.34.1

v2-0002-libpq-oauth-Don-t-translate-internal-mux-failures.patchapplication/octet-stream; name=v2-0002-libpq-oauth-Don-t-translate-internal-mux-failures.patchDownload
From 7542a1a6b72b89342375c0edd0d8655e51cc10cc Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 08:50:10 -0800
Subject: [PATCH v2 2/4] libpq-oauth: Don't translate internal mux failures
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Some error messages are generated when OAuth multiplexer operations fail
unexpectedly in the client. Álvaro pointed out that these are both
difficult to translate idiomatically (as they use internal terminology
heavily) and of dubious translation value to end users (since they're
going to need to get developer help anyway).

Remove these from the translation files by introducing an internal
variant of actx_error().

Suggested-by: Álvaro Herrera <alvherre@kurilemu.de>
Discussion:
Backpatch-through: ?
---
 src/interfaces/libpq-oauth/oauth-curl.c | 46 +++++++++++++------------
 1 file changed, 24 insertions(+), 22 deletions(-)

diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 8bd1870cf8a..83ab1068f2a 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -368,13 +368,16 @@ pg_fe_cleanup_oauth_flow(PGconn *conn)
 
 /*
  * Macros for manipulating actx->errbuf. actx_error() translates and formats a
- * string for you; actx_error_str() appends a string directly without
- * translation.
+ * string for you, actx_error_internal() is the untranslated equivalent, and
+ * actx_error_str() appends a string directly (also without translation).
  */
 
 #define actx_error(ACTX, FMT, ...) \
 	appendPQExpBuffer(&(ACTX)->errbuf, libpq_gettext(FMT), ##__VA_ARGS__)
 
+#define actx_error_internal(ACTX, FMT, ...) \
+	appendPQExpBuffer(&(ACTX)->errbuf, FMT, ##__VA_ARGS__)
+
 #define actx_error_str(ACTX, S) \
 	appendPQExpBufferStr(&(ACTX)->errbuf, S)
 
@@ -1180,20 +1183,20 @@ setup_multiplexer(struct async_ctx *actx)
 	actx->mux = epoll_create1(EPOLL_CLOEXEC);
 	if (actx->mux < 0)
 	{
-		actx_error(actx, "failed to create epoll set: %m");
+		actx_error_internal(actx, "failed to create epoll set: %m");
 		return false;
 	}
 
 	actx->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
 	if (actx->timerfd < 0)
 	{
-		actx_error(actx, "failed to create timerfd: %m");
+		actx_error_internal(actx, "failed to create timerfd: %m");
 		return false;
 	}
 
 	if (epoll_ctl(actx->mux, EPOLL_CTL_ADD, actx->timerfd, &ev) < 0)
 	{
-		actx_error(actx, "failed to add timerfd to epoll set: %m");
+		actx_error_internal(actx, "failed to add timerfd to epoll set: %m");
 		return false;
 	}
 
@@ -1202,8 +1205,7 @@ setup_multiplexer(struct async_ctx *actx)
 	actx->mux = kqueue();
 	if (actx->mux < 0)
 	{
-		/*- translator: the term "kqueue" (kernel queue) should not be translated */
-		actx_error(actx, "failed to create kqueue: %m");
+		actx_error_internal(actx, "failed to create kqueue: %m");
 		return false;
 	}
 
@@ -1216,7 +1218,7 @@ setup_multiplexer(struct async_ctx *actx)
 	actx->timerfd = kqueue();
 	if (actx->timerfd < 0)
 	{
-		actx_error(actx, "failed to create timer kqueue: %m");
+		actx_error_internal(actx, "failed to create timer kqueue: %m");
 		return false;
 	}
 
@@ -1260,7 +1262,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 			break;
 
 		default:
-			actx_error(actx, "unknown libcurl socket operation: %d", what);
+			actx_error_internal(actx, "unknown libcurl socket operation: %d", what);
 			return -1;
 	}
 
@@ -1277,15 +1279,15 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 		switch (op)
 		{
 			case EPOLL_CTL_ADD:
-				actx_error(actx, "could not add to epoll set: %m");
+				actx_error_internal(actx, "could not add to epoll set: %m");
 				break;
 
 			case EPOLL_CTL_DEL:
-				actx_error(actx, "could not delete from epoll set: %m");
+				actx_error_internal(actx, "could not delete from epoll set: %m");
 				break;
 
 			default:
-				actx_error(actx, "could not update epoll set: %m");
+				actx_error_internal(actx, "could not update epoll set: %m");
 		}
 
 		return -1;
@@ -1335,7 +1337,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 			break;
 
 		default:
-			actx_error(actx, "unknown libcurl socket operation: %d", what);
+			actx_error_internal(actx, "unknown libcurl socket operation: %d", what);
 			return -1;
 	}
 
@@ -1345,7 +1347,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 	res = kevent(actx->mux, ev, nev, ev_out, nev, &timeout);
 	if (res < 0)
 	{
-		actx_error(actx, "could not modify kqueue: %m");
+		actx_error_internal(actx, "could not modify kqueue: %m");
 		return -1;
 	}
 
@@ -1369,10 +1371,10 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 			switch (what)
 			{
 				case CURL_POLL_REMOVE:
-					actx_error(actx, "could not delete from kqueue: %m");
+					actx_error_internal(actx, "could not delete from kqueue: %m");
 					break;
 				default:
-					actx_error(actx, "could not add to kqueue: %m");
+					actx_error_internal(actx, "could not add to kqueue: %m");
 			}
 			return -1;
 		}
@@ -1421,7 +1423,7 @@ comb_multiplexer(struct async_ctx *actx)
 	 */
 	if (kevent(actx->mux, NULL, 0, &ev, 1, &timeout) < 0)
 	{
-		actx_error(actx, "could not comb kqueue: %m");
+		actx_error_internal(actx, "could not comb kqueue: %m");
 		return false;
 	}
 
@@ -1471,7 +1473,7 @@ set_timer(struct async_ctx *actx, long timeout)
 
 	if (timerfd_settime(actx->timerfd, 0 /* no flags */ , &spec, NULL) < 0)
 	{
-		actx_error(actx, "setting timerfd to %ld: %m", timeout);
+		actx_error_internal(actx, "setting timerfd to %ld: %m", timeout);
 		return false;
 	}
 
@@ -1501,14 +1503,14 @@ set_timer(struct async_ctx *actx, long timeout)
 	EV_SET(&ev, 1, EVFILT_TIMER, EV_DELETE, 0, 0, 0);
 	if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT)
 	{
-		actx_error(actx, "deleting kqueue timer: %m");
+		actx_error_internal(actx, "deleting kqueue timer: %m");
 		return false;
 	}
 
 	EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_DELETE, 0, 0, 0);
 	if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT)
 	{
-		actx_error(actx, "removing kqueue timer from multiplexer: %m");
+		actx_error_internal(actx, "removing kqueue timer from multiplexer: %m");
 		return false;
 	}
 
@@ -1519,14 +1521,14 @@ set_timer(struct async_ctx *actx, long timeout)
 	EV_SET(&ev, 1, EVFILT_TIMER, (EV_ADD | EV_ONESHOT), 0, timeout, 0);
 	if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0)
 	{
-		actx_error(actx, "setting kqueue timer to %ld: %m", timeout);
+		actx_error_internal(actx, "setting kqueue timer to %ld: %m", timeout);
 		return false;
 	}
 
 	EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_ADD, 0, 0, 0);
 	if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0)
 	{
-		actx_error(actx, "adding kqueue timer to multiplexer: %m");
+		actx_error_internal(actx, "adding kqueue timer to multiplexer: %m");
 		return false;
 	}
 
-- 
2.34.1

v2-0003-libpq-Align-oauth_json_set_error-with-other-NLS-p.patchapplication/octet-stream; name=v2-0003-libpq-Align-oauth_json_set_error-with-other-NLS-p.patchDownload
From 536eb8b08a2feb65966930b9bab1894a5fcbefc2 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 15:18:23 -0800
Subject: [PATCH v2 3/4] libpq: Align oauth_json_set_error() with other NLS
 patterns

Now that the prior commits have fixed missing OAuth translations, pull
the bespoke usage of libpq_gettext() for OAUTHBEARER parsing into
oauth_json_set_error() itself, and make that a gettext trigger as well,
to better match what the other sites are doing. Add an _internal()
variant to handle the existing untranslated case.

Suggested-by: Daniel Gustafsson <daniel@yesql.se>
Discussion: https://postgr.es/m/0EEBCAA8-A5AC-4E3B-BABA-0BA7A08C361B%40yesql.se
Backpatch-through: ?
---
 src/interfaces/libpq/fe-auth-oauth.c | 31 +++++++++++++++++-----------
 src/interfaces/libpq/nls.mk          |  2 ++
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index d146c5f567c..fb63a3249d6 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -183,7 +183,14 @@ struct json_ctx
 #define oauth_json_has_error(ctx) \
 	(PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg)
 
-#define oauth_json_set_error(ctx, ...) \
+#define oauth_json_set_error(ctx, fmt, ...) \
+	do { \
+		appendPQExpBuffer(&(ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__); \
+		(ctx)->errmsg = (ctx)->errbuf.data; \
+	} while (0)
+
+/* An untranslated version of oauth_json_set_error(). */
+#define oauth_json_set_error_internal(ctx, ...) \
 	do { \
 		appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \
 		(ctx)->errmsg = (ctx)->errbuf.data; \
@@ -199,13 +206,13 @@ oauth_json_object_start(void *state)
 		Assert(ctx->nested == 1);
 
 		oauth_json_set_error(ctx,
-							 libpq_gettext("field \"%s\" must be a string"),
+							 "field \"%s\" must be a string",
 							 ctx->target_field_name);
 	}
 
 	++ctx->nested;
 	if (ctx->nested > MAX_SASL_NESTING_LEVEL)
-		oauth_json_set_error(ctx, libpq_gettext("JSON is too deeply nested"));
+		oauth_json_set_error(ctx, "JSON is too deeply nested");
 
 	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
 }
@@ -254,20 +261,20 @@ oauth_json_array_start(void *state)
 
 	if (!ctx->nested)
 	{
-		ctx->errmsg = libpq_gettext("top-level element must be an object");
+		oauth_json_set_error(ctx, "top-level element must be an object");
 	}
 	else if (ctx->target_field)
 	{
 		Assert(ctx->nested == 1);
 
 		oauth_json_set_error(ctx,
-							 libpq_gettext("field \"%s\" must be a string"),
+							 "field \"%s\" must be a string",
 							 ctx->target_field_name);
 	}
 
 	++ctx->nested;
 	if (ctx->nested > MAX_SASL_NESTING_LEVEL)
-		oauth_json_set_error(ctx, libpq_gettext("JSON is too deeply nested"));
+		oauth_json_set_error(ctx, "JSON is too deeply nested");
 
 	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
 }
@@ -288,7 +295,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 
 	if (!ctx->nested)
 	{
-		ctx->errmsg = libpq_gettext("top-level element must be an object");
+		oauth_json_set_error(ctx, "top-level element must be an object");
 		return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -301,9 +308,9 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 			 * Assert and don't continue any further for production builds.
 			 */
 			Assert(false);
-			oauth_json_set_error(ctx,
-								 "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
-								 ctx->nested);
+			oauth_json_set_error_internal(ctx,
+										  "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
+										  ctx->nested);
 			return JSON_SEM_ACTION_FAILED;
 		}
 
@@ -314,7 +321,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 		if (*ctx->target_field)
 		{
 			oauth_json_set_error(ctx,
-								 libpq_gettext("field \"%s\" is duplicated"),
+								 "field \"%s\" is duplicated",
 								 ctx->target_field_name);
 			return JSON_SEM_ACTION_FAILED;
 		}
@@ -323,7 +330,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 		if (type != JSON_TOKEN_STRING)
 		{
 			oauth_json_set_error(ctx,
-								 libpq_gettext("field \"%s\" must be a string"),
+								 "field \"%s\" must be a string",
 								 ctx->target_field_name);
 			return JSON_SEM_ACTION_FAILED;
 		}
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 111e4849ed5..3fa87a0aaac 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -22,6 +22,7 @@ GETTEXT_TRIGGERS = actx_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext \
                    libpq_ngettext:1,2 \
+                   oauth_json_set_error:2 \
                    oauth_parse_set_error:2 \
                    pqInternalNotice:2
 GETTEXT_FLAGS    = actx_error:2:c-format \
@@ -30,5 +31,6 @@ GETTEXT_FLAGS    = actx_error:2:c-format \
                    libpq_gettext:1:pass-c-format \
                    libpq_ngettext:1:pass-c-format \
                    libpq_ngettext:2:pass-c-format \
+                   oauth_json_set_error:2:c-format \
                    oauth_parse_set_error:2:c-format \
                    pqInternalNotice:2:c-format
-- 
2.34.1

v2-0004-DO-NOT-MERGE-squash-libpq-Add-missing-OAuth-trans.patchapplication/octet-stream; name=v2-0004-DO-NOT-MERGE-squash-libpq-Add-missing-OAuth-trans.patchDownload
From 1a27987ce885b7979d316ef59f2f819eca152880 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 14:02:04 -0800
Subject: [PATCH v2 4/4] DO NOT MERGE: squash! libpq: Add missing OAuth
 translations

Similarly to the first commit in this set, jsonapi.c wasn't being
considered for the libpq translation files. Follow the lead of 3ddbac368
and pull it in.

Unfortunately this doesn't work in practice, because jsonapi.c doesn't
use the libpq-specific text domain. More fixes required.
---
 src/interfaces/libpq/nls.mk | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 3fa87a0aaac..dfbbdc88046 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -15,9 +15,11 @@ GETTEXT_FILES    = fe-auth.c \
                    fe-secure-gssapi.c \
                    fe-secure-openssl.c \
                    win32.c \
+                   ../../common/jsonapi.c \
                    ../libpq-oauth/oauth-curl.c \
                    ../libpq-oauth/oauth-utils.c
 GETTEXT_TRIGGERS = actx_error:2 \
+                   json_token_error:2 \
                    libpq_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext \
@@ -26,6 +28,7 @@ GETTEXT_TRIGGERS = actx_error:2 \
                    oauth_parse_set_error:2 \
                    pqInternalNotice:2
 GETTEXT_FLAGS    = actx_error:2:c-format \
+                   json_token_error:2:c-format \
                    libpq_append_conn_error:2:c-format \
                    libpq_append_error:2:c-format \
                    libpq_gettext:1:pass-c-format \
-- 
2.34.1

#10Álvaro Herrera
alvherre@kurilemu.de
In reply to: Jacob Champion (#9)
Re: Few untranslated error messages in OAuth

Hello,

On 2025-Nov-13, Jacob Champion wrote:

On Thu, Nov 13, 2025 at 3:08 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I'm writing up a patch that combines all of that.

And here's what I have so far. I could use help double-checking the
.po result; I'm not entirely sure my usage of update-po is correct.

I'm not sure what you think you might be doing wrong, but as far as I
can tell, the .po is being generated correctly.

- v2-0001: combines Zhijie Hou's patch with the gettext_noop()
suggestion from Alvaro and fixes the nls.mk omission
- v2-0002: removes translation of multiplexer failures by adding an
_internal macro
- v2-0003: aligns oauth_json_set_error() with the prior commits

I gave these a quick look, and they look correct to me. I didn't test
the resulting libpq though.

There are a few single quotes in some messages, which we tend not to
use. I'd replace those with double quotes. Of those, as an example,
this one caught my attention for strange wording,
"internal error: field \"%s\" still active at end of object"
I think it means we haven't seen the closing quote at the end of the
field, right? Maybe say something like "unterminated field \"%s\" ..."?

There's also the strings in CHECK_MSETOPT and siblings macros missing
quotes -- should be
"failed to set \"%s\" on OAuth connection: %s"

- v2-0004: tries to get jsonapi.c translated too

Unfortunately v2-0004 doesn't work. It puts the messages into the
translation files, but we use the _() macro throughout jsonapi.c,
which isn't helpful for libpq because libpq uses its own text domain.
This was an oversight in the work done for 0785d1b8b, I think, and it
may need its own patchset unless someone has a really quick fix.

You're right, that's no good. We could try to define a new macro (maybe
jsonapi_gettext()) that does stock _() on backend but libpq_gettext() on
frontend ... but that wouldn't work nicely for frontend users other than
libpq. Maybe something like

#ifndef jsonapi_gettext
#ifdef FRONTEND
#define jsonapi_gettext(msg) libpq_gettext(msg)
#else
#define jsonapi_gettext(msg) gettext(msg)
#endif
#endif

so any callers that want a third definition can just define it
themselves in the compile line?

Thanks for your time on this,

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Cómo ponemos nuestros dedos en la arcilla del otro. Eso es la amistad; jugar
al alfarero y ver qué formas se pueden sacar del otro" (C. Halloway en
La Feria de las Tinieblas, R. Bradbury)

#11Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Álvaro Herrera (#10)
3 attachment(s)
Re: Few untranslated error messages in OAuth

On Thu, Nov 27, 2025 at 10:24 AM Álvaro Herrera <alvherre@kurilemu.de> wrote:

I gave these a quick look, and they look correct to me. I didn't test
the resulting libpq though.

Thanks for the review!

Of those, as an example,
this one caught my attention for strange wording,
"internal error: field \"%s\" still active at end of object"
I think it means we haven't seen the closing quote at the end of the
field, right? Maybe say something like "unterminated field \"%s\" ..."?

Oh, those catch logic errors in the parsing engine. v3-0002 removes
those from the translation files as well.

There's also the strings in CHECK_MSETOPT and siblings macros missing
quotes -- should be
"failed to set \"%s\" on OAuth connection: %s"

Personally I prefer bare %s there, since it's an option name. Compare

setsockopt(SO_REUSEADDR) failed
failed to set CURLMOPT_SOCKETDATA on OAuth connection

You're right, that's no good. We could try to define a new macro (maybe
jsonapi_gettext()) that does stock _() on backend but libpq_gettext() on
frontend ... but that wouldn't work nicely for frontend users other than
libpq. Maybe something like

#ifndef jsonapi_gettext
#ifdef FRONTEND
#define jsonapi_gettext(msg) libpq_gettext(msg)
#else
#define jsonapi_gettext(msg) gettext(msg)
#endif
#endif

so any callers that want a third definition can just define it
themselves in the compile line?

Yeah, the pattern should probably follow that of the
JSONAPI_USE_PQEXPBUFFER conditionals. I think I'll defer this until
after [1]/messages/by-id/CAOYmi+mrGg+n_X2MOLgeWcj3v_M00gR8uz_D7mM8z=dX1JYVbg@mail.gmail.com; otherwise I might need to solve it twice. 0004 has been
dropped from the set.

On Thu, Nov 13, 2025 at 4:58 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I assume translation changes such as these are generally
backportable?

For now, I'll proceed as if a backport to 18 is appropriate for these.

Thanks again!
--Jacob

[1]: /messages/by-id/CAOYmi+mrGg+n_X2MOLgeWcj3v_M00gR8uz_D7mM8z=dX1JYVbg@mail.gmail.com

Attachments:

v3-0001-libpq-Add-missing-OAuth-translations.patchapplication/octet-stream; name=v3-0001-libpq-Add-missing-OAuth-translations.patchDownload
From 9924f0432bb5e0dbdb46cd780f20d14c398baac6 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 12:42:00 -0800
Subject: [PATCH v3 1/3] libpq: Add missing OAuth translations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Several strings that should have been translated as they passed through
libpq_gettext were not actually being pulled into the translation files,
because I hadn't directly wrapped them in one of the GETTEXT_TRIGGERS.

Move the responsibility for calling libpq_gettext() to the code that
sets actx->errctx. Doing the same in report_type_mismatch() would result
in double-translation, so mark those strings with gettext_noop()
instead. And wrap two ternary operands with gettext_noop(), even though
they're already in one of the triggers, since xgettext sees only the
first.

Finally, fe-auth-oauth.c was missing from nls.mk, so none of that file
was being translated at all. Add it now.

Original patch by Zhijie Hou, plus suggested tweaks by Álvaro Herrera
and small additions by me.

Reported-by: Zhijie Hou <houzj.fnst@fujitsu.com>
Author: Zhijie Hou <houzj.fnst@fujitsu.com>
Co-authored-by: Álvaro Herrera <alvherre@kurilemu.de>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
Discussion: https://postgr.es/m/TY4PR01MB1690746DB91991D1E9A47F57E94CDA%40TY4PR01MB16907.jpnprd01.prod.outlook.com
Backpatch-through: 18
---
 src/interfaces/libpq-oauth/oauth-curl.c | 35 +++++++++++++------------
 src/interfaces/libpq/nls.mk             |  1 +
 2 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index bd0a656a166..c9765159260 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -247,7 +247,8 @@ struct async_ctx
 	 * our entry point, errors have three parts:
 	 *
 	 * - errctx:	an optional static string, describing the global operation
-	 *				currently in progress. It'll be translated for you.
+	 *				currently in progress. Should be translated with
+	 *				libpq_gettext().
 	 *
 	 * - errbuf:	contains the actual error message. Generally speaking, use
 	 *				actx_error[_str] to manipulate this. This must be filled
@@ -475,20 +476,20 @@ report_type_mismatch(struct oauth_parse *ctx)
 	switch (ctx->active->type)
 	{
 		case JSON_TOKEN_STRING:
-			msgfmt = "field \"%s\" must be a string";
+			msgfmt = gettext_noop("field \"%s\" must be a string");
 			break;
 
 		case JSON_TOKEN_NUMBER:
-			msgfmt = "field \"%s\" must be a number";
+			msgfmt = gettext_noop("field \"%s\" must be a number");
 			break;
 
 		case JSON_TOKEN_ARRAY_START:
-			msgfmt = "field \"%s\" must be an array of strings";
+			msgfmt = gettext_noop("field \"%s\" must be an array of strings");
 			break;
 
 		default:
 			Assert(false);
-			msgfmt = "field \"%s\" has unexpected type";
+			msgfmt = gettext_noop("field \"%s\" has unexpected type");
 	}
 
 	oauth_parse_set_error(ctx, msgfmt, ctx->active->name);
@@ -1087,7 +1088,7 @@ parse_token_error(struct async_ctx *actx, struct token_error *err)
 	 * override the errctx if parsing explicitly fails.
 	 */
 	if (!result)
-		actx->errctx = "failed to parse token error response";
+		actx->errctx = libpq_gettext("failed to parse token error response");
 
 	return result;
 }
@@ -1115,8 +1116,8 @@ record_token_error(struct async_ctx *actx, const struct token_error *err)
 		if (response_code == 401)
 		{
 			actx_error(actx, actx->used_basic_auth
-					   ? "provider rejected the oauth_client_secret"
-					   : "provider requires client authentication, and no oauth_client_secret is set");
+					   ? gettext_noop("provider rejected the oauth_client_secret")
+					   : gettext_noop("provider requires client authentication, and no oauth_client_secret is set"));
 			actx_error_str(actx, " ");
 		}
 	}
@@ -2153,7 +2154,7 @@ finish_discovery(struct async_ctx *actx)
 	/*
 	 * Pull the fields we care about from the document.
 	 */
-	actx->errctx = "failed to parse OpenID discovery document";
+	actx->errctx = libpq_gettext("failed to parse OpenID discovery document");
 	if (!parse_provider(actx, &actx->provider))
 		return false;			/* error message already set */
 
@@ -2421,7 +2422,7 @@ finish_device_authz(struct async_ctx *actx)
 	 */
 	if (response_code == 200)
 	{
-		actx->errctx = "failed to parse device authorization";
+		actx->errctx = libpq_gettext("failed to parse device authorization");
 		if (!parse_device_authz(actx, &actx->authz))
 			return false;		/* error message already set */
 
@@ -2509,7 +2510,7 @@ finish_token_request(struct async_ctx *actx, struct token *tok)
 	 */
 	if (response_code == 200)
 	{
-		actx->errctx = "failed to parse access token response";
+		actx->errctx = libpq_gettext("failed to parse access token response");
 		if (!parse_access_token(actx, tok))
 			return false;		/* error message already set */
 
@@ -2888,7 +2889,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 		switch (actx->step)
 		{
 			case OAUTH_STEP_INIT:
-				actx->errctx = "failed to fetch OpenID discovery document";
+				actx->errctx = libpq_gettext("failed to fetch OpenID discovery document");
 				if (!start_discovery(actx, conn_oauth_discovery_uri(conn)))
 					goto error_return;
 
@@ -2902,11 +2903,11 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				if (!check_issuer(actx, conn))
 					goto error_return;
 
-				actx->errctx = "cannot run OAuth device authorization";
+				actx->errctx = libpq_gettext("cannot run OAuth device authorization");
 				if (!check_for_device_flow(actx))
 					goto error_return;
 
-				actx->errctx = "failed to obtain device authorization";
+				actx->errctx = libpq_gettext("failed to obtain device authorization");
 				if (!start_device_authz(actx, conn))
 					goto error_return;
 
@@ -2917,7 +2918,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				if (!finish_device_authz(actx))
 					goto error_return;
 
-				actx->errctx = "failed to obtain access token";
+				actx->errctx = libpq_gettext("failed to obtain access token");
 				if (!start_token_request(actx, conn))
 					goto error_return;
 
@@ -2968,7 +2969,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
 				break;
 
 			case OAUTH_STEP_WAIT_INTERVAL:
-				actx->errctx = "failed to obtain access token";
+				actx->errctx = libpq_gettext("failed to obtain access token");
 				if (!start_token_request(actx, conn))
 					goto error_return;
 
@@ -2994,7 +2995,7 @@ error_return:
 	 * also the documentation for struct async_ctx.
 	 */
 	if (actx->errctx)
-		appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx));
+		appendPQExpBuffer(errbuf, "%s: ", actx->errctx);
 
 	if (PQExpBufferDataBroken(actx->errbuf))
 		appendPQExpBufferStr(errbuf, libpq_gettext("out of memory"));
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index b87df277d93..111e4849ed5 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,7 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
 GETTEXT_FILES    = fe-auth.c \
+                   fe-auth-oauth.c \
                    fe-auth-scram.c \
                    fe-cancel.c \
                    fe-connect.c \
-- 
2.34.1

v3-0002-libpq-oauth-Don-t-translate-internal-errors.patchapplication/octet-stream; name=v3-0002-libpq-oauth-Don-t-translate-internal-errors.patchDownload
From 6e1e8b2477beddf8c7bc0a44acf7dd21cb55caae Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 08:50:10 -0800
Subject: [PATCH v3 2/3] libpq-oauth: Don't translate internal errors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Some error messages are generated when OAuth multiplexer operations fail
unexpectedly in the client. Álvaro pointed out that these are both
difficult to translate idiomatically (as they use internal terminology
heavily) and of dubious translation value to end users (since they're
going to need to get developer help anyway). The response parsing engine
has a similar issue.

Remove these from the translation files by introducing internal variants
of actx_error() and oauth_parse_set_error().

Suggested-by: Álvaro Herrera <alvherre@kurilemu.de>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
Discussion: https://postgr.es/m/CAOYmi%2BkQQ8vpRcoSrA5EQ98Wa3G6jFj1yRHs6mh1V7ohkTC7JA%40mail.gmail.com
Backpatch-through: 18
---
 src/interfaces/libpq-oauth/oauth-curl.c | 85 +++++++++++++------------
 1 file changed, 45 insertions(+), 40 deletions(-)

diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index c9765159260..92a7f1cd383 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -368,13 +368,16 @@ pg_fe_cleanup_oauth_flow(PGconn *conn)
 
 /*
  * Macros for manipulating actx->errbuf. actx_error() translates and formats a
- * string for you; actx_error_str() appends a string directly without
- * translation.
+ * string for you, actx_error_internal() is the untranslated equivalent, and
+ * actx_error_str() appends a string directly (also without translation).
  */
 
 #define actx_error(ACTX, FMT, ...) \
 	appendPQExpBuffer(&(ACTX)->errbuf, libpq_gettext(FMT), ##__VA_ARGS__)
 
+#define actx_error_internal(ACTX, FMT, ...) \
+	appendPQExpBuffer(&(ACTX)->errbuf, FMT, ##__VA_ARGS__)
+
 #define actx_error_str(ACTX, S) \
 	appendPQExpBufferStr(&(ACTX)->errbuf, S)
 
@@ -462,6 +465,9 @@ struct oauth_parse
 #define oauth_parse_set_error(ctx, fmt, ...) \
 	appendPQExpBuffer((ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__)
 
+#define oauth_parse_set_error_internal(ctx, fmt, ...) \
+	appendPQExpBuffer((ctx)->errbuf, fmt, ##__VA_ARGS__)
+
 static void
 report_type_mismatch(struct oauth_parse *ctx)
 {
@@ -537,9 +543,9 @@ oauth_json_object_field_start(void *state, char *name, bool isnull)
 		if (ctx->active)
 		{
 			Assert(false);
-			oauth_parse_set_error(ctx,
-								  "internal error: started field '%s' before field '%s' was finished",
-								  name, ctx->active->name);
+			oauth_parse_set_error_internal(ctx,
+										   "internal error: started field \"%s\" before field \"%s\" was finished",
+										   name, ctx->active->name);
 			return JSON_SEM_ACTION_FAILED;
 		}
 
@@ -589,9 +595,9 @@ oauth_json_object_end(void *state)
 	if (!ctx->nested && ctx->active)
 	{
 		Assert(false);
-		oauth_parse_set_error(ctx,
-							  "internal error: field '%s' still active at end of object",
-							  ctx->active->name);
+		oauth_parse_set_error_internal(ctx,
+									   "internal error: field \"%s\" still active at end of object",
+									   ctx->active->name);
 		return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -645,9 +651,9 @@ oauth_json_array_end(void *state)
 		if (ctx->nested != 2 || ctx->active->type != JSON_TOKEN_ARRAY_START)
 		{
 			Assert(false);
-			oauth_parse_set_error(ctx,
-								  "internal error: found unexpected array end while parsing field '%s'",
-								  ctx->active->name);
+			oauth_parse_set_error_internal(ctx,
+										   "internal error: found unexpected array end while parsing field \"%s\"",
+										   ctx->active->name);
 			return JSON_SEM_ACTION_FAILED;
 		}
 
@@ -700,9 +706,9 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 			if (ctx->nested != 1)
 			{
 				Assert(false);
-				oauth_parse_set_error(ctx,
-									  "internal error: scalar target found at nesting level %d",
-									  ctx->nested);
+				oauth_parse_set_error_internal(ctx,
+											   "internal error: scalar target found at nesting level %d",
+											   ctx->nested);
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -710,9 +716,9 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 			if (*field->scalar)
 			{
 				Assert(false);
-				oauth_parse_set_error(ctx,
-									  "internal error: scalar field '%s' would be assigned twice",
-									  ctx->active->name);
+				oauth_parse_set_error_internal(ctx,
+											   "internal error: scalar field \"%s\" would be assigned twice",
+											   ctx->active->name);
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -732,9 +738,9 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 			if (ctx->nested != 2)
 			{
 				Assert(false);
-				oauth_parse_set_error(ctx,
-									  "internal error: array member found at nesting level %d",
-									  ctx->nested);
+				oauth_parse_set_error_internal(ctx,
+											   "internal error: array member found at nesting level %d",
+											   ctx->nested);
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -1180,20 +1186,20 @@ setup_multiplexer(struct async_ctx *actx)
 	actx->mux = epoll_create1(EPOLL_CLOEXEC);
 	if (actx->mux < 0)
 	{
-		actx_error(actx, "failed to create epoll set: %m");
+		actx_error_internal(actx, "failed to create epoll set: %m");
 		return false;
 	}
 
 	actx->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
 	if (actx->timerfd < 0)
 	{
-		actx_error(actx, "failed to create timerfd: %m");
+		actx_error_internal(actx, "failed to create timerfd: %m");
 		return false;
 	}
 
 	if (epoll_ctl(actx->mux, EPOLL_CTL_ADD, actx->timerfd, &ev) < 0)
 	{
-		actx_error(actx, "failed to add timerfd to epoll set: %m");
+		actx_error_internal(actx, "failed to add timerfd to epoll set: %m");
 		return false;
 	}
 
@@ -1202,8 +1208,7 @@ setup_multiplexer(struct async_ctx *actx)
 	actx->mux = kqueue();
 	if (actx->mux < 0)
 	{
-		/*- translator: the term "kqueue" (kernel queue) should not be translated */
-		actx_error(actx, "failed to create kqueue: %m");
+		actx_error_internal(actx, "failed to create kqueue: %m");
 		return false;
 	}
 
@@ -1216,7 +1221,7 @@ setup_multiplexer(struct async_ctx *actx)
 	actx->timerfd = kqueue();
 	if (actx->timerfd < 0)
 	{
-		actx_error(actx, "failed to create timer kqueue: %m");
+		actx_error_internal(actx, "failed to create timer kqueue: %m");
 		return false;
 	}
 
@@ -1260,7 +1265,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 			break;
 
 		default:
-			actx_error(actx, "unknown libcurl socket operation: %d", what);
+			actx_error_internal(actx, "unknown libcurl socket operation: %d", what);
 			return -1;
 	}
 
@@ -1277,15 +1282,15 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 		switch (op)
 		{
 			case EPOLL_CTL_ADD:
-				actx_error(actx, "could not add to epoll set: %m");
+				actx_error_internal(actx, "could not add to epoll set: %m");
 				break;
 
 			case EPOLL_CTL_DEL:
-				actx_error(actx, "could not delete from epoll set: %m");
+				actx_error_internal(actx, "could not delete from epoll set: %m");
 				break;
 
 			default:
-				actx_error(actx, "could not update epoll set: %m");
+				actx_error_internal(actx, "could not update epoll set: %m");
 		}
 
 		return -1;
@@ -1335,7 +1340,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 			break;
 
 		default:
-			actx_error(actx, "unknown libcurl socket operation: %d", what);
+			actx_error_internal(actx, "unknown libcurl socket operation: %d", what);
 			return -1;
 	}
 
@@ -1345,7 +1350,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 	res = kevent(actx->mux, ev, nev, ev_out, nev, &timeout);
 	if (res < 0)
 	{
-		actx_error(actx, "could not modify kqueue: %m");
+		actx_error_internal(actx, "could not modify kqueue: %m");
 		return -1;
 	}
 
@@ -1369,10 +1374,10 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx,
 			switch (what)
 			{
 				case CURL_POLL_REMOVE:
-					actx_error(actx, "could not delete from kqueue: %m");
+					actx_error_internal(actx, "could not delete from kqueue: %m");
 					break;
 				default:
-					actx_error(actx, "could not add to kqueue: %m");
+					actx_error_internal(actx, "could not add to kqueue: %m");
 			}
 			return -1;
 		}
@@ -1421,7 +1426,7 @@ comb_multiplexer(struct async_ctx *actx)
 	 */
 	if (kevent(actx->mux, NULL, 0, &ev, 1, &timeout) < 0)
 	{
-		actx_error(actx, "could not comb kqueue: %m");
+		actx_error_internal(actx, "could not comb kqueue: %m");
 		return false;
 	}
 
@@ -1471,7 +1476,7 @@ set_timer(struct async_ctx *actx, long timeout)
 
 	if (timerfd_settime(actx->timerfd, 0 /* no flags */ , &spec, NULL) < 0)
 	{
-		actx_error(actx, "setting timerfd to %ld: %m", timeout);
+		actx_error_internal(actx, "setting timerfd to %ld: %m", timeout);
 		return false;
 	}
 
@@ -1501,14 +1506,14 @@ set_timer(struct async_ctx *actx, long timeout)
 	EV_SET(&ev, 1, EVFILT_TIMER, EV_DELETE, 0, 0, 0);
 	if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT)
 	{
-		actx_error(actx, "deleting kqueue timer: %m");
+		actx_error_internal(actx, "deleting kqueue timer: %m");
 		return false;
 	}
 
 	EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_DELETE, 0, 0, 0);
 	if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT)
 	{
-		actx_error(actx, "removing kqueue timer from multiplexer: %m");
+		actx_error_internal(actx, "removing kqueue timer from multiplexer: %m");
 		return false;
 	}
 
@@ -1519,14 +1524,14 @@ set_timer(struct async_ctx *actx, long timeout)
 	EV_SET(&ev, 1, EVFILT_TIMER, (EV_ADD | EV_ONESHOT), 0, timeout, 0);
 	if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0)
 	{
-		actx_error(actx, "setting kqueue timer to %ld: %m", timeout);
+		actx_error_internal(actx, "setting kqueue timer to %ld: %m", timeout);
 		return false;
 	}
 
 	EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_ADD, 0, 0, 0);
 	if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0)
 	{
-		actx_error(actx, "adding kqueue timer to multiplexer: %m");
+		actx_error_internal(actx, "adding kqueue timer to multiplexer: %m");
 		return false;
 	}
 
-- 
2.34.1

v3-0003-libpq-Align-oauth_json_set_error-with-other-NLS-p.patchapplication/octet-stream; name=v3-0003-libpq-Align-oauth_json_set_error-with-other-NLS-p.patchDownload
From eedc7c41a9d96e123d7f599e4f24c310177e3a41 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 13 Nov 2025 15:18:23 -0800
Subject: [PATCH v3 3/3] libpq: Align oauth_json_set_error() with other NLS
 patterns
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Now that the prior commits have fixed missing OAuth translations, pull
the bespoke usage of libpq_gettext() for OAUTHBEARER parsing into
oauth_json_set_error() itself, and make that a gettext trigger as well,
to better match what the other sites are doing. Add an _internal()
variant to handle the existing untranslated case.

Suggested-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
Discussion: https://postgr.es/m/0EEBCAA8-A5AC-4E3B-BABA-0BA7A08C361B%40yesql.se
Backpatch-through: 18
---
 src/interfaces/libpq/fe-auth-oauth.c | 31 +++++++++++++++++-----------
 src/interfaces/libpq/nls.mk          |  2 ++
 2 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index d146c5f567c..fb63a3249d6 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -183,7 +183,14 @@ struct json_ctx
 #define oauth_json_has_error(ctx) \
 	(PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg)
 
-#define oauth_json_set_error(ctx, ...) \
+#define oauth_json_set_error(ctx, fmt, ...) \
+	do { \
+		appendPQExpBuffer(&(ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__); \
+		(ctx)->errmsg = (ctx)->errbuf.data; \
+	} while (0)
+
+/* An untranslated version of oauth_json_set_error(). */
+#define oauth_json_set_error_internal(ctx, ...) \
 	do { \
 		appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \
 		(ctx)->errmsg = (ctx)->errbuf.data; \
@@ -199,13 +206,13 @@ oauth_json_object_start(void *state)
 		Assert(ctx->nested == 1);
 
 		oauth_json_set_error(ctx,
-							 libpq_gettext("field \"%s\" must be a string"),
+							 "field \"%s\" must be a string",
 							 ctx->target_field_name);
 	}
 
 	++ctx->nested;
 	if (ctx->nested > MAX_SASL_NESTING_LEVEL)
-		oauth_json_set_error(ctx, libpq_gettext("JSON is too deeply nested"));
+		oauth_json_set_error(ctx, "JSON is too deeply nested");
 
 	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
 }
@@ -254,20 +261,20 @@ oauth_json_array_start(void *state)
 
 	if (!ctx->nested)
 	{
-		ctx->errmsg = libpq_gettext("top-level element must be an object");
+		oauth_json_set_error(ctx, "top-level element must be an object");
 	}
 	else if (ctx->target_field)
 	{
 		Assert(ctx->nested == 1);
 
 		oauth_json_set_error(ctx,
-							 libpq_gettext("field \"%s\" must be a string"),
+							 "field \"%s\" must be a string",
 							 ctx->target_field_name);
 	}
 
 	++ctx->nested;
 	if (ctx->nested > MAX_SASL_NESTING_LEVEL)
-		oauth_json_set_error(ctx, libpq_gettext("JSON is too deeply nested"));
+		oauth_json_set_error(ctx, "JSON is too deeply nested");
 
 	return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
 }
@@ -288,7 +295,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 
 	if (!ctx->nested)
 	{
-		ctx->errmsg = libpq_gettext("top-level element must be an object");
+		oauth_json_set_error(ctx, "top-level element must be an object");
 		return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -301,9 +308,9 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 			 * Assert and don't continue any further for production builds.
 			 */
 			Assert(false);
-			oauth_json_set_error(ctx,
-								 "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
-								 ctx->nested);
+			oauth_json_set_error_internal(ctx,
+										  "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
+										  ctx->nested);
 			return JSON_SEM_ACTION_FAILED;
 		}
 
@@ -314,7 +321,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 		if (*ctx->target_field)
 		{
 			oauth_json_set_error(ctx,
-								 libpq_gettext("field \"%s\" is duplicated"),
+								 "field \"%s\" is duplicated",
 								 ctx->target_field_name);
 			return JSON_SEM_ACTION_FAILED;
 		}
@@ -323,7 +330,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type)
 		if (type != JSON_TOKEN_STRING)
 		{
 			oauth_json_set_error(ctx,
-								 libpq_gettext("field \"%s\" must be a string"),
+								 "field \"%s\" must be a string",
 								 ctx->target_field_name);
 			return JSON_SEM_ACTION_FAILED;
 		}
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 111e4849ed5..3fa87a0aaac 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -22,6 +22,7 @@ GETTEXT_TRIGGERS = actx_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext \
                    libpq_ngettext:1,2 \
+                   oauth_json_set_error:2 \
                    oauth_parse_set_error:2 \
                    pqInternalNotice:2
 GETTEXT_FLAGS    = actx_error:2:c-format \
@@ -30,5 +31,6 @@ GETTEXT_FLAGS    = actx_error:2:c-format \
                    libpq_gettext:1:pass-c-format \
                    libpq_ngettext:1:pass-c-format \
                    libpq_ngettext:2:pass-c-format \
+                   oauth_json_set_error:2:c-format \
                    oauth_parse_set_error:2:c-format \
                    pqInternalNotice:2:c-format
-- 
2.34.1

#12Álvaro Herrera
alvherre@kurilemu.de
In reply to: Jacob Champion (#11)
Re: Few untranslated error messages in OAuth

Hello,

These patches look good to me.

On 2025-Dec-11, Jacob Champion wrote:

Oh, those catch logic errors in the parsing engine. v3-0002 removes
those from the translation files as well.

Sounds good.

On Thu, Nov 27, 2025 at 10:24 AM Álvaro Herrera <alvherre@kurilemu.de> wrote:

There's also the strings in CHECK_MSETOPT and siblings macros missing
quotes -- should be
"failed to set \"%s\" on OAuth connection: %s"

Personally I prefer bare %s there, since it's an option name. Compare

setsockopt(SO_REUSEADDR) failed
failed to set CURLMOPT_SOCKETDATA on OAuth connection

Hmm, okay.

Yeah, the pattern should probably follow that of the
JSONAPI_USE_PQEXPBUFFER conditionals. I think I'll defer this until
after [1]; otherwise I might need to solve it twice. 0004 has been
dropped from the set.

[1] /messages/by-id/CAOYmi+mrGg+n_X2MOLgeWcj3v_M00gR8uz_D7mM8z=dX1JYVbg@mail.gmail.com

Sure, no objections to that plan. I'd say these messages are probably
among the most important ones to translate in the OAuth support in
libpq, because as I understand some of them are more likely to become
part of a GUI that an end-user interacts with. But I have no quarrels
with it all waiting until a later release. (After all, early adopters
are going to force English on themselves anyway.)

No strong opinion on JSONAPI_USE_PQEXPBUFFER. As far as I can tell, we
pretty much force you to link libpq if you want to have a PQExpBuffer,
which tells me that a frontend jsonapi.c user would already be forced to
link libpq. So they will also have libpq_gettext(). So maybe a dual
answer is realistic -- no need to consider a third case of frontend
jsonapi.c without libpq. If somebody in the future wants to use
jsonapi.c without libpq, they can do the translation API fix work then.

On Thu, Nov 13, 2025 at 4:58 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I assume translation changes such as these are generally
backportable?

For now, I'll proceed as if a backport to 18 is appropriate for these.

Yeah, I'd prefer that.

--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"The Gord often wonders why people threaten never to come back after they've
been told never to return" (www.actsofgord.com)

#13Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Álvaro Herrera (#12)
Re: Few untranslated error messages in OAuth

On Fri, Dec 12, 2025 at 2:09 AM Álvaro Herrera <alvherre@kurilemu.de> wrote:

No strong opinion on JSONAPI_USE_PQEXPBUFFER. As far as I can tell, we
pretty much force you to link libpq if you want to have a PQExpBuffer,
which tells me that a frontend jsonapi.c user would already be forced to
link libpq.

Only the _shlib variant uses JSONAPI_USE_PQEXPBUFFER; the "main"
frontend version doesn't. (pg_combinebackup is an example of a
frontend JSON client that doesn't link against libpq, I think.)

For now, I'll proceed as if a backport to 18 is appropriate for these.

Yeah, I'd prefer that.

Committed and backpatched. Thanks for the reviews, thanks everybody
for the thread, and thank you Hou-san for the patch!

--Jacob