On Fri, 2025-07-11 at 11:48 +1200, Thomas Munro wrote:
> On Fri, Jul 11, 2025 at 6:22 AM Jeff Davis <pg...@j-davis.com> wrote:
> > I don't have a great windows development environment, and it
> > appears CI
> > and the buildfarm don't offer great coverage either. Can I ask for
> > a
> > volunteer to do the windows side of this work?
> 
> Me neither but I'm willing to help with that, and have done lots of
> closely related things through trial-by-CI...

Attached a patch to separate the message translation (both gettext and
strerror translations) from setlocale(). That's a step towards thread
safety, and also a step toward setting LC_CTYPE=C permanently (more
work still required there).

The patch feels a bit over-engineered, but I'd like to know what you
think. It would be great if you could test/debug the windows NLS-
enabled paths.

I'm also not sure what to do about the NetBSD path. NetBSD has no
uselocale(), so I have to fall bad to temporary setlocale(), which is
not thread safe. And I'm getting a mysterious error in test_aio for
NetBSD, which I haven't investigated yet.

Regards,
        Jeff Davis

From 8bd59e7d52351fadeb4fe26023aa0ff57735e03f Mon Sep 17 00:00:00 2001
From: Jeff Davis <j...@j-davis.com>
Date: Fri, 18 Jul 2025 14:06:45 -0700
Subject: [PATCH v5] Create wrapper for managing NLS locale.

Message translation depends on LC_CTYPE and LC_MESSAGES. Use wrapper
functions to control those settings rather than relying on the
permanent setlocale() settings.

Improves thread safety by using "_l()" variants of functions or
uselocale() where available. On windows, setlocale() can be made
thread-safe. There is still at least one platform (NetBSD) where none
of those options are available, in which case it still depends on
thread-unsafe setlocale().

Also separates message translation behavior from other, unrelated
behaviors like tolower().

Discussion: https://postgr.es/m/f040113cf384ada69558ec004a04a3ddb3e40a26.ca...@j-davis.com
---
 configure.ac                      |   2 +
 meson.build                       |   2 +
 src/backend/main/main.c           |  13 +-
 src/backend/utils/adt/Makefile    |   1 +
 src/backend/utils/adt/meson.build |   1 +
 src/backend/utils/adt/pg_locale.c |  39 +--
 src/backend/utils/adt/pg_nls.c    | 417 ++++++++++++++++++++++++++++++
 src/backend/utils/init/postinit.c |   4 +
 src/include/c.h                   |  19 +-
 src/include/pg_config.h.in        |   8 +
 src/include/port.h                |  10 +
 src/include/utils/pg_nls.h        |  29 +++
 src/tools/pg_bsd_indent/err.c     |   2 +
 13 files changed, 524 insertions(+), 23 deletions(-)
 create mode 100644 src/backend/utils/adt/pg_nls.c
 create mode 100644 src/include/utils/pg_nls.h

diff --git a/configure.ac b/configure.ac
index c2877e36935..493014302cd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1791,6 +1791,8 @@ AC_CHECK_FUNCS(m4_normalize([
 	backtrace_symbols
 	copyfile
 	copy_file_range
+	dgettext_l
+	dngettext_l
 	elf_aux_info
 	getauxval
 	getifaddrs
diff --git a/meson.build b/meson.build
index 5365aaf95e6..d3c285b2d54 100644
--- a/meson.build
+++ b/meson.build
@@ -2882,6 +2882,8 @@ func_checks = [
   # when enabling asan the dlopen check doesn't notice that -ldl is actually
   # required. Just checking for dlsym() ought to suffice.
   ['dlsym', {'dependencies': [dl_dep], 'define': false}],
+  ['dgettext_l'],
+  ['dngettext_l'],
   ['elf_aux_info'],
   ['explicit_bzero'],
   ['getauxval'],
diff --git a/src/backend/main/main.c b/src/backend/main/main.c
index bdcb5e4f261..fbef0245b28 100644
--- a/src/backend/main/main.c
+++ b/src/backend/main/main.c
@@ -38,6 +38,7 @@
 #include "utils/help_config.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/pg_nls.h"
 #include "utils/ps_status.h"
 
 
@@ -139,14 +140,16 @@ main(int argc, char *argv[])
 	init_locale("LC_CTYPE", LC_CTYPE, "");
 
 	/*
-	 * LC_MESSAGES will get set later during GUC option processing, but we set
-	 * it here to allow startup error messages to be localized.
+	 * Initialize NLS locale's LC_CTYPE and LC_MESSAGES from the environment.
+	 * It will be updated later during GUC option processing, but we set it
+	 * here to allow startup error messages to be localized.
 	 */
-#ifdef LC_MESSAGES
-	init_locale("LC_MESSAGES", LC_MESSAGES, "");
-#endif
+	pg_nls_set_locale("", "");
 
 	/* We keep these set to "C" always.  See pg_locale.c for explanation. */
+#ifdef LC_MESSAGES
+	init_locale("LC_MESSAGES", LC_MESSAGES, "C");
+#endif
 	init_locale("LC_MONETARY", LC_MONETARY, "C");
 	init_locale("LC_NUMERIC", LC_NUMERIC, "C");
 	init_locale("LC_TIME", LC_TIME, "C");
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index ffeacf2b819..38e395b7de9 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -84,6 +84,7 @@ OBJS = \
 	pg_locale_icu.o \
 	pg_locale_libc.o \
 	pg_lsn.o \
+	pg_nls.o \
 	pg_upgrade_support.o \
 	pgstatfuncs.o \
 	pseudorandomfuncs.o \
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index ed9bbd7b926..f85436cd766 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -71,6 +71,7 @@ backend_sources += files(
   'pg_locale_icu.c',
   'pg_locale_libc.c',
   'pg_lsn.c',
+  'pg_nls.c',
   'pg_upgrade_support.c',
   'pgstatfuncs.c',
   'pseudorandomfuncs.c',
diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 97c2ac1faf9..39c06b91e7d 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -10,21 +10,29 @@
  */
 
 /*----------
- * Here is how the locale stuff is handled: LC_COLLATE and LC_CTYPE
- * are fixed at CREATE DATABASE time, stored in pg_database, and cannot
- * be changed. Thus, the effects of strcoll(), strxfrm(), isupper(),
- * toupper(), etc. are always in the same fixed locale.
+ * Here is how the locale stuff is handled:
  *
- * LC_MESSAGES is settable at run time and will take effect
- * immediately.
+ * LC_COLLATE is permanently set to "C" with setlocale(), and collation
+ * behavior is defined entirely by pg_locale_t, which has provider-dependent
+ * behavior. If the provider is libc, then it holds a locale_t object with
+ * LC_COLLATE set appropriately.
  *
- * The other categories, LC_MONETARY, LC_NUMERIC, and LC_TIME are
- * permanently set to "C", and then we use temporary locale_t
- * objects when we need to look up locale data based on the GUCs
- * of the same name.  Information is cached when the GUCs change.
- * The cached information is only used by the formatting functions
- * (to_char, etc.) and the money type.  For the user, this should all be
- * transparent.
+ * LC_CTYPE is fixed at CREATE DATABASE time, stored in pg_database, set at
+ * database connection time with setlocale(), and cannot be changed. The
+ * effects are limited, because casing and character classification is mostly
+ * defined by pg_locale_t, and message encoding is controlled by
+ * pg_nls_set_locale(). LC_CTYPE does affect a few places in the backend, such
+ * as case conversions where a pg_locale_t object is unavailable.
+ *
+ * LC_MESSAGES is permanently set to "C" with setlocale(), and NLS behavior is
+ * controlled with pg_nls_set_locale().
+ *
+ * The other categories, LC_MONETARY, LC_NUMERIC, and LC_TIME are permanently
+ * set to "C" with setlocale(), and then we use temporary locale_t objects
+ * when we need to look up locale data based on the GUCs of the same name.
+ * Information is cached when the GUCs change.  The cached information is only
+ * used by the formatting functions (to_char, etc.) and the money type.  For
+ * the user, this should all be transparent.
  *----------
  */
 
@@ -45,6 +53,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/pg_nls.h"
 #include "utils/relcache.h"
 #include "utils/syscache.h"
 
@@ -405,9 +414,7 @@ assign_locale_messages(const char *newval, void *extra)
 	 * LC_MESSAGES category does not exist everywhere, but accept it anyway.
 	 * We ignore failure, as per comment above.
 	 */
-#ifdef LC_MESSAGES
-	(void) pg_perm_setlocale(LC_MESSAGES, newval);
-#endif
+	pg_nls_set_locale(NULL, newval);
 }
 
 
diff --git a/src/backend/utils/adt/pg_nls.c b/src/backend/utils/adt/pg_nls.c
new file mode 100644
index 00000000000..0276da880d6
--- /dev/null
+++ b/src/backend/utils/adt/pg_nls.c
@@ -0,0 +1,417 @@
+/*-----------------------------------------------------------------------
+ *
+ * PostgreSQL NLS utilities
+ *
+ * Portions Copyright (c) 2002-2025, PostgreSQL Global Development Group
+ *
+ * src/backend/utils/adt/pg_nls.c
+ *
+ * Platform-independent wrappers for message translation functions. The
+ * LC_CTYPE and LC_MESSAGES settings are set with pg_nls_set_locale() and the
+ * state is managed internally to this file, regardless of the outside
+ * settings from setlocale() or uselocale().
+ *
+ * The implementation prefers the "_l()" variants of functions, then
+ * secondarily a temporary uselocale() setting (thread safe), and lastly a
+ * temporary setlocale() setting (which can be made thread safe on windows).
+ *
+ * This mechanism improves thread safety (on most platforms), and provides
+ * better separation between the behavior of NLS and other behaviors like
+ * isupper(), etc.
+ *
+ *-----------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/memutils.h"
+#include "utils/pg_locale.h"
+#include "utils/pg_nls.h"
+
+/*
+ * Represents global LC_CTYPE and LC_MESSAGES settings, for the purpose of
+ * message translation. LC_CTYPE in the postmaster comes from the environment,
+ * and in a backend comes from pg_database.datctype. LC_MESSAGES comes from a
+ * GUC, and must be kept up to date.
+ *
+ * If there's no uselocale(), keep the string values instead, and use
+ * setlocale().
+ */
+#ifdef HAVE_USELOCALE
+
+static locale_t nls_locale = (locale_t) 0;
+
+#else
+
+static char *nls_lc_ctype = NULL;
+static char *nls_lc_messages = NULL;
+
+typedef struct SaveLocale
+{
+#ifndef WIN32
+	char	   *lc_ctype;
+	char	   *lc_messages;
+#else
+	int			config_thread_locale;
+	wchar_t    *lc_ctype;
+	wchar_t    *lc_messages;
+#endif							/* WIN32 */
+}			SaveLocale;
+
+#endif							/* !HAVE_USELOCALE */
+
+/*
+ * Set the LC_CTYPE and LC_MESSAGES to be used for message translation.
+ */
+void
+pg_nls_set_locale(const char *ctype, const char *messages)
+{
+	if (ctype)
+	{
+#ifdef HAVE_USELOCALE
+		locale_t	loc = 0;
+
+		errno = 0;
+		loc = newlocale(LC_CTYPE_MASK, ctype, nls_locale);
+		if (!loc)
+			report_newlocale_failure(ctype);
+		nls_locale = loc;
+#else
+		if (!check_locale(LC_CTYPE, ctype, NULL))
+			report_newlocale_failure(ctype);
+		if (nls_lc_ctype)
+			pfree(nls_lc_ctype);
+		nls_lc_ctype = MemoryContextStrdup(TopMemoryContext, ctype);
+#endif
+
+		/*
+		 * Use the right encoding in translated messages.  Under ENABLE_NLS,
+		 * let pg_bind_textdomain_codeset() figure it out.  Under !ENABLE_NLS,
+		 * message format strings are ASCII, but database-encoding strings may
+		 * enter the message via %s.  This makes the overall message encoding
+		 * equal to the database encoding.
+		 */
+#ifdef ENABLE_NLS
+		SetMessageEncoding(pg_bind_textdomain_codeset(textdomain(NULL)));
+#else
+		SetMessageEncoding(GetDatabaseEncoding());
+#endif
+	}
+
+	if (messages)
+	{
+#ifdef HAVE_USELOCALE
+		locale_t	loc = 0;
+
+		errno = 0;
+		loc = newlocale(LC_MESSAGES_MASK, messages, nls_locale);
+		if (!loc)
+			report_newlocale_failure(messages);
+		nls_locale = loc;
+#else
+#ifdef LC_MESSAGES
+		if (!check_locale(LC_MESSAGES, messages, NULL))
+			report_newlocale_failure(messages);
+#endif
+		if (nls_lc_messages)
+			pfree(nls_lc_messages);
+		nls_lc_messages = MemoryContextStrdup(TopMemoryContext, messages);
+#endif
+	}
+}
+
+#ifdef ENABLE_NLS
+
+#ifdef HAVE_USELOCALE
+
+#ifndef HAVE_DGETTEXT_L
+static char *
+dgettext_l(const char *domainname, const char *msgid, locale_t loc)
+{
+	char	   *result;
+	locale_t	save_loc = uselocale(loc);
+
+	result = dcgettext(domainname, msgid, LC_MESSAGES);
+	uselocale(save_loc);
+	return result;
+}
+#endif							/* HAVE_DGETTEXT_L */
+
+#ifndef HAVE_DNGETTEXT_L
+static char *
+dngettext_l(const char *domainname, const char *s, const char *p,
+			unsigned long int n, locale_t loc)
+{
+	char	   *result;
+	locale_t	save_loc = uselocale(loc);
+
+	result = dcngettext(domainname, s, p, n, LC_MESSAGES);
+	uselocale(save_loc);
+	return result;
+}
+#endif							/* HAVE_DNGETTEXT_L */
+
+static char *
+pg_strerror_l(int errnum, locale_t loc)
+{
+	char	   *result;
+	locale_t	save_loc = uselocale(loc);
+
+	result = pg_strerror(errnum);
+	uselocale(save_loc);
+	return result;
+}
+
+static char *
+pg_strerror_r_l(int errnum, char *buf, size_t buflen, locale_t loc)
+{
+	char	   *result;
+	locale_t	save_loc = uselocale(loc);
+
+	result = pg_strerror_r(errnum, buf, buflen);
+	uselocale(save_loc);
+	return result;
+}
+
+#else							/* !HAVE_USELOCALE */
+
+static bool
+save_message_locale(SaveLocale * save)
+{
+#ifndef WIN32
+	char	   *tmp;
+
+	/*
+	 * This path -- ENABLE_NLS, !HAVE_USELOCALE, !WIN32 -- is not thread safe,
+	 * but is only known to be used on NetBSD.
+	 */
+	tmp = setlocale(LC_CTYPE, NULL);
+	if (!tmp)
+		return false;
+	save->lc_ctype = pstrdup(tmp);
+
+	tmp = setlocale(LC_MESSAGES, NULL);
+	if (!tmp)
+		return false;
+	save->lc_messages = pstrdup(tmp);
+
+	return true;
+#else
+	wchar_t    *tmp;
+
+	/* Put setlocale() into thread-local mode. */
+	save->config_thread_locale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
+
+	/*
+	 * Capture the current values as wide strings.  Otherwise, we might not be
+	 * able to restore them if their names contain non-ASCII characters and
+	 * the intermediate locale changes the expected encoding.  We don't want
+	 * to leave the caller in an unexpected state by failing to restore, or
+	 * crash the runtime library.
+	 */
+	tmp = _wsetlocale(LC_CTYPE, NULL);
+	if (!tmp || !(tmp = wcsdup(tmp)))
+		return false;
+	*save->lc_ctype = tmp;
+
+	tmp = _wsetlocale(LC_MESSAGES, NULL);
+	if (!tmp || !(tmp = wcsdup(tmp)))
+		return false;
+	*save->lc_messages = tmp;
+
+	return true;
+#endif
+}
+
+static void
+restore_message_locale(SaveLocale * save)
+{
+#ifndef WIN32
+	if (save->lc_ctype)
+	{
+		setlocale(LC_CTYPE, save->lc_ctype);
+		pfree(save->lc_ctype);
+		save->lc_ctype = NULL;
+	}
+	if (save->lc_messages)
+	{
+		setlocale(LC_MESSAGES, save->lc_messages);
+		pfree(save->lc_messages);
+		save->lc_messages = NULL;
+	}
+#else
+	if (save->lc_ctype)
+	{
+		_wsetlocale(LC_CTYPE, save->lc_ctype);
+		free(save->lc_ctype);
+		save->lc_ctype = NULL;
+	}
+	if (save->lc_messages)
+	{
+		_wsetlocale(LC_MESSAGES, save->lc_messages);
+		free(save->lc_messages);
+		save->lc_messages = NULL;
+	}
+	_configthreadlocale(save->config_thread_locale);
+#endif
+}
+
+static char *
+dgettext_l(const char *domainname, const char *msgid, const char *lc_ctype,
+		   const char *lc_messages)
+{
+	SaveLocale	save;
+
+	if (save_message_locale(&save))
+	{
+		char	   *result;
+
+		(void) setlocale(LC_CTYPE, lc_ctype);
+		(void) setlocale(LC_MESSAGES, lc_messages);
+
+		result = dcgettext(domainname, msgid, LC_MESSAGES);
+		restore_message_locale(&save);
+		return result;
+	}
+	else
+		return dcgettext(domainname, msgid, LC_MESSAGES);
+}
+
+static char *
+dngettext_l(const char *domainname, const char *s, const char *p,
+			unsigned long int n, const char *lc_ctype,
+			const char *lc_messages)
+{
+	SaveLocale	save;
+
+	if (save_message_locale(&save))
+	{
+		char	   *result;
+
+		(void) setlocale(LC_CTYPE, lc_ctype);
+		(void) setlocale(LC_MESSAGES, lc_messages);
+
+		result = dcngettext(domainname, s, p, n, LC_MESSAGES);
+		restore_message_locale(&save);
+		return result;
+	}
+	else
+		return dcngettext(domainname, s, p, n, LC_MESSAGES);
+}
+
+static char *
+pg_strerror_l(int errnum, const char *lc_ctype, const char *lc_messages)
+{
+	SaveLocale	save;
+
+	if (save_message_locale(&save))
+	{
+		char	   *result;
+
+		(void) setlocale(LC_CTYPE, lc_ctype);
+		(void) setlocale(LC_MESSAGES, lc_messages);
+
+		result = pg_strerror(errnum);
+		restore_message_locale(&save);
+		return result;
+	}
+	else
+		return pg_strerror(errnum);
+}
+
+static char *
+pg_strerror_r_l(int errnum, char *buf, size_t buflen, const char *lc_ctype,
+				const char *lc_messages)
+{
+	SaveLocale	save;
+
+	if (save_message_locale(&save))
+	{
+		char	   *result;
+
+		(void) setlocale(LC_CTYPE, lc_ctype);
+		(void) setlocale(LC_MESSAGES, lc_messages);
+
+		result = pg_strerror_r(errnum, buf, buflen);
+		restore_message_locale(&save);
+		return result;
+	}
+	else
+		return pg_strerror_r(errnum, buf, buflen);
+}
+
+#endif							/* !HAVE_USELOCALE */
+
+/*
+ * dgettext() with nls_locale, if set.
+ */
+char *
+pg_nls_dgettext(const char *domainname, const char *msgid)
+{
+#ifdef HAVE_USELOCALE
+	if (nls_locale)
+		return dgettext_l(domainname, msgid, nls_locale);
+#else
+	if (nls_lc_ctype)
+		return dgettext_l(domainname, msgid, nls_lc_ctype,
+						  nls_lc_messages);
+#endif
+	else
+		return dcgettext(domainname, msgid, LC_MESSAGES);
+}
+
+/*
+ * dngettext() with nls_locale, if set.
+ */
+char *
+pg_nls_dngettext(const char *domainname, const char *s, const char *p,
+				 unsigned long int n)
+{
+#ifdef HAVE_USELOCALE
+	if (nls_locale)
+		return dngettext_l(domainname, s, p, n, nls_locale);
+#else
+	if (nls_lc_ctype)
+		return dngettext_l(domainname, s, p, n, nls_lc_ctype,
+						   nls_lc_messages);
+#endif
+	else
+		return dcngettext(domainname, s, p, n, LC_MESSAGES);
+}
+
+/*
+ * pg_strerror() with nls_locale, if set.
+ */
+char *
+pg_nls_strerror(int errnum)
+{
+#ifdef HAVE_USELOCALE
+	if (nls_locale)
+		return pg_strerror_l(errnum, nls_locale);
+#else
+	if (nls_lc_ctype)
+		return pg_strerror_l(errnum, nls_lc_ctype, nls_lc_messages);
+#endif
+	else
+		return pg_strerror(errnum);
+}
+
+/*
+ * pg_strerror_r() with nls_locale, if set.
+ */
+char *
+pg_nls_strerror_r(int errnum, char *buf, size_t buflen)
+{
+#ifdef HAVE_USELOCALE
+	if (nls_locale)
+		return pg_strerror_r_l(errnum, buf, buflen, nls_locale);
+#else
+	if (nls_lc_ctype)
+		return pg_strerror_r_l(errnum, buf, buflen, nls_lc_ctype,
+							   nls_lc_messages);
+#endif
+	else
+		return pg_strerror_r(errnum, buf, buflen);
+}
+
+#endif							/* ENABLE_NLS */
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..3206dd121ed 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -65,6 +65,7 @@
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
 #include "utils/portal.h"
+#include "utils/pg_nls.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
@@ -430,6 +431,9 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
 						   " which is not recognized by setlocale().", ctype),
 				 errhint("Recreate the database with another locale or install the missing locale.")));
 
+	/* set global_message_locale for this database to datctype */
+	pg_nls_set_locale(ctype, NULL);
+
 	if (strcmp(ctype, "C") == 0 ||
 		strcmp(ctype, "POSIX") == 0)
 		database_ctype_is_c = true;
diff --git a/src/include/c.h b/src/include/c.h
index 6d4495bdd9f..2edb8e2f63d 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -1159,8 +1159,23 @@ typedef union PGAlignedXLogBlock
  * gettext support
  */
 
-#ifndef ENABLE_NLS
-/* stuff we'd otherwise get from <libintl.h> */
+#if defined(ENABLE_NLS) && !defined(FRONTEND)
+/* use backend's global message locale setting */
+#include "utils/pg_nls.h"
+
+#undef gettext
+#undef dgettext
+#undef ngettext
+#undef dngettext
+
+#define gettext(x) pg_nls_dgettext(NULL, x)
+#define dgettext(d,x) pg_nls_dgettext(d, x)
+#define ngettext(s,p,n) pg_nls_dngettext(NULL, s, p, n)
+#define dngettext(d,s,p,n) pg_nls_dngettext(d, s, p, n)
+#elif defined(ENABLE_NLS)
+/* use <libintl.h> directly */
+#else
+/* no-op */
 #define gettext(x) (x)
 #define dgettext(d,x) (x)
 #define ngettext(s,p,n) ((n) == 1 ? (s) : (p))
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c4dc5d72bdb..f2fa336bd95 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -111,6 +111,14 @@
    don't. */
 #undef HAVE_DECL_STRCHRNUL
 
+/* Define to 1 if you have the declaration of `dgettext_l', and to 0 if you
+   don't. */
+#undef HAVE_DGETTEXT_L
+
+/* Define to 1 if you have the declaration of `dngettext_l', and to 0 if you
+   don't. */
+#undef HAVE_DNGETTEXT_L
+
 /* Define to 1 if you have the declaration of `strlcat', and to 0 if you
    don't. */
 #undef HAVE_DECL_STRLCAT
diff --git a/src/include/port.h b/src/include/port.h
index 3964d3b1293..c00d84d0dbf 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -249,11 +249,21 @@ extern int	pg_strfromd(char *str, size_t count, int precision, double value);
 
 /* Replace strerror() with our own, somewhat more robust wrapper */
 extern char *pg_strerror(int errnum);
+#if defined(ENABLE_NLS) && !defined(FRONTEND)
+extern char *pg_nls_strerror(int errnum);
+#define strerror pg_nls_strerror
+#else
 #define strerror pg_strerror
+#endif
 
 /* Likewise for strerror_r(); note we prefer the GNU API for that */
 extern char *pg_strerror_r(int errnum, char *buf, size_t buflen);
+#if defined(ENABLE_NLS) && !defined(FRONTEND)
+extern char *pg_nls_strerror_r(int errnum, char *buf, size_t buflen);
+#define strerror_r pg_nls_strerror_r
+#else
 #define strerror_r pg_strerror_r
+#endif
 #define PG_STRERROR_R_BUFLEN 256	/* Recommended buffer size for strerror_r */
 
 /* Wrap strsignal(), or provide our own version if necessary */
diff --git a/src/include/utils/pg_nls.h b/src/include/utils/pg_nls.h
new file mode 100644
index 00000000000..c8c605e9f26
--- /dev/null
+++ b/src/include/utils/pg_nls.h
@@ -0,0 +1,29 @@
+/*-----------------------------------------------------------------------
+ *
+ * PostgreSQL NLS utilities
+ *
+ * src/include/utils/pg_nls.h
+ *
+ * Copyright (c) 2002-2025, PostgreSQL Global Development Group
+ *
+ *-----------------------------------------------------------------------
+ */
+
+#ifndef _PG_NLS_
+#define _PG_NLS_
+
+extern void pg_nls_set_locale(const char *ctype, const char *messages);
+
+#ifdef ENABLE_NLS
+
+extern char *pg_nls_dgettext(const char *domainname, const char *msgid)
+			pg_attribute_format_arg(2);
+extern char *pg_nls_dngettext(const char *domainname, const char *s,
+							  const char *p, unsigned long int n)
+			pg_attribute_format_arg(2) pg_attribute_format_arg(3);
+extern char *pg_nls_strerror(int errnum);
+extern char *pg_nls_strerror_r(int errnum, char *buf, size_t buflen);
+
+#endif
+
+#endif							/* _PG_NLS_ */
diff --git a/src/tools/pg_bsd_indent/err.c b/src/tools/pg_bsd_indent/err.c
index 807319334bc..fe153aa3dcd 100644
--- a/src/tools/pg_bsd_indent/err.c
+++ b/src/tools/pg_bsd_indent/err.c
@@ -27,6 +27,8 @@
  * SUCH DAMAGE.
  */
 
+#define FRONTEND 1
+
 /*
  * This is cut down to just the minimum that we need to build indent.
  */
-- 
2.43.0

Reply via email to