]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
gettext-runtime: New programs 'printf_gettext', 'printf_ngettext'.
authorBruno Haible <bruno@clisp.org>
Wed, 25 Jun 2025 02:16:03 +0000 (04:16 +0200)
committerBruno Haible <bruno@clisp.org>
Wed, 25 Jun 2025 02:16:03 +0000 (04:16 +0200)
* autogen.sh (GNULIB_MODULES_RUNTIME_FOR_SRC): Add c-ctype, c-strtold,
fzprintf-posix, mbrtoc32, mbszero, quote, stdint-h, strtoimax, strtold,
strtoumax, xstrtold.
* gettext-runtime/src/printf-command.h: New file.
* gettext-runtime/src/printf-command.c: New file.
* gettext-runtime/src/printf_gettext.c: New file.
* gettext-runtime/src/Makefile.am (bin_PROGRAMS): Add printf_gettext,
printf_ngettext.
(noinst_LIBRARIES, libgrtsrc_a_SOURCES): New variables.
(printf_gettext_SOURCES, printf_gettext_CFLAGS, printf_gettext_LDFLAGS): New
variables.
(printf_ngettext_SOURCES, printf_ngettext_CFLAGS, printf_ngettext_LDFLAGS): New
variables.
(LDADD): Add libgrtsrc.a.
* gettext-runtime/po/POTFILES.in: Add src/printf-command.c,
src/printf_gettext.c, src/printf_ngettext.c.
* gettext-runtime/man/printf_gettext.x: New file.
* gettext-runtime/man/printf_ngettext.x: New file.
* gettext-runtime/man/Makefile.am (man_aux): Add printf_gettext.x,
printf_ngettext.x.
(man_MAN1GEN): Add printf_gettext.1, printf_ngettext.1.
(man_MAN1IN): Add printf_gettext.1.in, printf_ngettext.1.in.
(man_HTML1GEN): Add printf_gettext.1.html, printf_ngettext.1.html.
(man_HTML1IN): Add printf_gettext.1.html.in, printf_ngettext.1.html.in.
(printf_gettext.1, rintf_ngettext.1, printf_gettext.1.in, printf_ngettext.1.in):
Add dependencies.
(printf_gettext.1.html, printf_ngettext.1.html, printf_gettext.1.html.in,
printf_ngettext.1.html.in): Likewise.
* gettext-runtime/Makefile.am (distdir1): Depend on man/printf_gettext.1 and
man/printf_ngettext.1.
(man/printf_gettext.1, man/printf_ngettext.1): Depend on gen-man1.
(gen-man1): Make src/printf_gettext, src/printf_ngettext and printf_gettext.1,
printf_ngettext.1.
* gettext-runtime/doc/rt-printf_gettext.texi: New file.
* gettext-runtime/doc/rt-printf_ngettext.texi: New file.
* gettext-runtime/doc/Makefile.am (EXTRA_DIST): Add them.
* gettext-tools/doc/lang-sh.texi (printf_gettext Invocation,
printf_ngettext Invocation): New subsubsections.
* gettext-tools/doc/gettext.texi (@direntry): Add printf_gettext, .
* gettext-tools/doc/Makefile.am (gettext_TEXINFOS): Add rt-printf_gettext.texi,
rt-printf_ngettext.texi.
* gettext-runtime/NEWS: Mention the new programs.
* NEWS: Likewise.
* PACKAGING: Add the printf_gettext and printf_ngettext programs and their
documentation.

22 files changed:
.gitignore
Admin/release-steps
NEWS
PACKAGING
autogen.sh
gettext-runtime/Makefile.am
gettext-runtime/NEWS
gettext-runtime/doc/Makefile.am
gettext-runtime/doc/rt-printf_gettext.texi [new file with mode: 0644]
gettext-runtime/doc/rt-printf_ngettext.texi [new file with mode: 0644]
gettext-runtime/man/Makefile.am
gettext-runtime/man/printf_gettext.x [new file with mode: 0644]
gettext-runtime/man/printf_ngettext.x [new file with mode: 0644]
gettext-runtime/po/POTFILES.in
gettext-runtime/src/Makefile.am
gettext-runtime/src/printf-command.c [new file with mode: 0644]
gettext-runtime/src/printf-command.h [new file with mode: 0644]
gettext-runtime/src/printf_gettext.c [new file with mode: 0644]
gettext-runtime/src/printf_ngettext.c [new file with mode: 0644]
gettext-tools/doc/Makefile.am
gettext-tools/doc/gettext.texi
gettext-tools/doc/lang-sh.texi

index a520ac084a76ea7ad2ff32ee0db7c63df65e279c..ca70c6b64ad10e9c1fe343d8e3b3a9bc314e414f 100644 (file)
 /gettext-runtime/man/gettext.1.html.in
 /gettext-runtime/man/ngettext.1.in
 /gettext-runtime/man/ngettext.1.html.in
+/gettext-runtime/man/printf_gettext.1.in
+/gettext-runtime/man/printf_gettext.1.html.in
+/gettext-runtime/man/printf_ngettext.1.in
+/gettext-runtime/man/printf_ngettext.1.html.in
 /gettext-runtime/man/bind_textdomain_codeset.3
 /gettext-runtime/man/bind_textdomain_codeset.3.html
 /gettext-runtime/man/bindtextdomain.3
@@ -699,6 +703,10 @@ autom4te.cache/
 /gettext-runtime/man/gettext.1.html
 /gettext-runtime/man/ngettext.1
 /gettext-runtime/man/ngettext.1.html
+/gettext-runtime/man/printf_gettext.1
+/gettext-runtime/man/printf_gettext.1.html
+/gettext-runtime/man/printf_ngettext.1
+/gettext-runtime/man/printf_ngettext.1.html
 /gettext-tools/libgrep/libgrep.a
 /gettext-tools/src/**/*.class
 /gettext-tools/src/gettext.jar
@@ -716,6 +724,10 @@ autom4te.cache/
 /gettext-runtime/src/gettext.exe
 /gettext-runtime/src/ngettext
 /gettext-runtime/src/ngettext.exe
+/gettext-runtime/src/printf_gettext
+/gettext-runtime/src/printf_gettext.exe
+/gettext-runtime/src/printf_ngettext
+/gettext-runtime/src/printf_ngettext.exe
 /gettext-runtime/tests/test-lock
 /gettext-runtime/tests/test-lock.exe
 /gettext-tools/libgettextpo/gettext-po.h
index 0aebdd2581771a495351491b7233251213e52f67..b95e759479485786d2ac18b08a2333e545d77bca 100644 (file)
@@ -121,6 +121,8 @@ We assume that the following environment variables are set:
     gettext-runtime/src/gettext.c
     gettext-runtime/src/gettext.sh.in
     gettext-runtime/src/ngettext.c
+    gettext-runtime/src/printf_gettext.c
+    gettext-runtime/src/printf_ngettext.c
     gettext-tools/misc/autopoint.in
     gettext-tools/misc/convert-archive.in
     gettext-tools/misc/gettextize.in
diff --git a/NEWS b/NEWS
index 0b7218c6be5de1b0c601a8672844d376036fa336..2394045e864fa90b0c4afa1651c54c194f0c8083 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,9 @@ Version 0.26 - July 2025
       They are marked as 'sh-printf-format' in POT and PO files.
     - xgettext now recognizes the \c, \u, and \U escape sequences in dollar-
       single-quoted strings $'...'.
+    - Two new programs 'printf_gettext' and 'printf_ngettext' are provided,
+      that do formatted output with a localized format string in a more
+      efficient way (without spawning a subshell).
 
 # Bug fixes:
   - The AM_GNU_GETTEXT macro now rejects the dysfunctional gettext() function
index 1ae8fe827b9d0f80ace233dcb337aa369d38d92b..73fb94dc03b2ee28ac3b160f60588af5aabc76f9 100644 (file)
--- a/PACKAGING
+++ b/PACKAGING
@@ -76,13 +76,19 @@ the following file list.
 
       $prefix/bin/gettext
       $prefix/bin/ngettext
+      $prefix/bin/printf_gettext
+      $prefix/bin/printf_ngettext
       $prefix/bin/envsubst
       $prefix/bin/gettext.sh
       $prefix/share/man/man1/gettext.1
       $prefix/share/man/man1/ngettext.1
+      $prefix/share/man/man1/printf_gettext.1
+      $prefix/share/man/man1/printf_ngettext.1
       $prefix/share/man/man1/envsubst.1
       $prefix/share/doc/gettext/gettext.1.html
       $prefix/share/doc/gettext/ngettext.1.html
+      $prefix/share/doc/gettext/printf_gettext.1.html
+      $prefix/share/doc/gettext/printf_ngettext.1.html
       $prefix/share/doc/gettext/envsubst.1.html
       $prefix/share/locale/*/LC_MESSAGES/gettext-runtime.mo
 
index cac02dedc1fbb7645451fdc5ac4a72aaaddb221a..320b626d263e8f1e387dcf5722b97a087cf8d940 100755 (executable)
@@ -101,25 +101,36 @@ if ! $skip_gnulib; then
     basename-lgpl
     binary-io
     bool
+    c-ctype
+    c-strtold
     closeout
     error
+    fzprintf-posix
     getopt-gnu
     gettext-h
     havelib
+    mbrtoc32
+    mbszero
     memmove
     noreturn
     progname
     propername
+    quote
     relocatable-prog
     setlocale
     sigpipe
+    stdint-h
     stdio-h
     stdlib-h
+    strtoimax
+    strtold
     strtoul
+    strtoumax
     unistd-h
     unlocked-io
     xalloc
     xstring-buffer
+    xstrtold
   '
   GNULIB_MODULES_RUNTIME_OTHER='
     gettext-runtime-misc
index 86c0ea754e3c0c002d297e2026ec50c9f37b4cdc..18946894161f4d86f19cba12072a50ce6ae1fcac 100644 (file)
@@ -52,15 +52,17 @@ EXTRA_DIST += INSTALL.windows
 # Hidden from automake, but really activated. Works around an automake bug.
 #distdir: distdir1
 .PHONY: distdir1
-distdir1: man/gettext.1 man/ngettext.1 man/envsubst.1
-man/gettext.1 man/ngettext.1 man/envsubst.1: gen-man1
+distdir1: man/gettext.1 man/ngettext.1 man/printf_gettext.1 man/printf_ngettext.1 man/envsubst.1
+man/gettext.1 man/ngettext.1 man/printf_gettext.1 man/printf_ngettext.1 man/envsubst.1: gen-man1
 .PHONY: gen-man1
 gen-man1: src/gettext.c man/gettext.x \
           src/ngettext.c man/ngettext.x \
+          src/printf_gettext.c man/printf_gettext.x \
+          src/printf_ngettext.c man/printf_ngettext.x \
           src/envsubst.c man/envsubst.x
        cd gnulib-lib && $(MAKE) $(AM_MAKEFLAGS)
-       cd src && $(MAKE) $(AM_MAKEFLAGS) gettext$(EXEEXT) ngettext$(EXEEXT) envsubst$(EXEEXT)
-       cd man && $(MAKE) $(AM_MAKEFLAGS) gettext.1 ngettext.1 envsubst.1
+       cd src && $(MAKE) $(AM_MAKEFLAGS) gettext$(EXEEXT) ngettext$(EXEEXT) printf_gettext$(EXEEXT) printf_ngettext$(EXEEXT) envsubst$(EXEEXT)
+       cd man && $(MAKE) $(AM_MAKEFLAGS) gettext.1 ngettext.1 printf_gettext.1 printf_ngettext.1 envsubst.1
 
 
 maintainer-update-po: $(top_builddir)/config.status
index b6ec64fdea9bcc6885e9c2b3a87ea64408cc1f3e..8a007c0db598c02e420b47f80d499ad71b77a96a 100644 (file)
@@ -1,3 +1,9 @@
+Version 0.26 - July 2025
+
+* Two new programs 'printf_gettext' and 'printf_ngettext' are provided,
+  that do formatted output with a localized format string in a more
+  efficient way (without spawning a subshell).
+
 Version 0.25 - April 2025
 
 * New library: libintl_d.a contains the runtime for using GNU gettext
index b65ccd0c5548cdf973632f2f512f48499f9c0f24..812020d3ef0f311d3f0f79eb485fe4c8b5ae5d6d 100644 (file)
@@ -1,5 +1,5 @@
 ## Makefile for the gettext-runtime/doc subdirectory of GNU gettext
-## Copyright (C) 1995-1997, 2001-2003, 2006 Free Software Foundation, Inc.
+## Copyright (C) 1995-2025 Free Software Foundation, Inc.
 ##
 ## This program is free software: you can redistribute it and/or modify
 ## it under the terms of the GNU General Public License as published by
@@ -21,4 +21,9 @@ EXTRA_DIST =
 
 EXTRA_DIST += nls.texi matrix.texi
 
-EXTRA_DIST += rt-gettext.texi rt-ngettext.texi rt-envsubst.texi
+EXTRA_DIST += \
+  rt-gettext.texi \
+  rt-ngettext.texi \
+  rt-printf_gettext.texi \
+  rt-printf_ngettext.texi \
+  rt-envsubst.texi
diff --git a/gettext-runtime/doc/rt-printf_gettext.texi b/gettext-runtime/doc/rt-printf_gettext.texi
new file mode 100644 (file)
index 0000000..8e9871c
--- /dev/null
@@ -0,0 +1,141 @@
+@c This file is part of the GNU gettext manual.
+@c Copyright (C) 2025 Free Software Foundation, Inc.
+@c See the file gettext.texi for copying conditions.
+
+@pindex printf_gettext
+@cindex @code{printf_gettext} program, usage
+@example
+printf_gettext [@var{option}] @var{format} [@var{argument}]...
+@end example
+
+@cindex lookup format string translation
+@cindex formatted output in Shell
+The @code{printf_gettext} program produces formatted output,
+applying the native language translation of @var{format}
+to the @var{argument}s.
+
+@noindent @strong{Options and arguments}
+
+@table @samp
+@item -c @var{context}
+@itemx --context=@var{context}
+@opindex -c@r{, @code{printf_gettext} option}
+@opindex --context@r{, @code{printf_gettext} option}
+Specify the context for the format string to be translated.
+See @ref{Contexts} for details.
+
+@item @var{format}
+The format string.
+
+@item @var{argument}
+A string or numeric argument.
+
+@end table
+
+@noindent @strong{Informative output}
+
+@table @samp
+@item -h
+@itemx --help
+@opindex -h@r{, @code{printf_gettext} option}
+@opindex --help@r{, @code{printf_gettext} option}
+Display this help and exit.
+
+@item -V
+@itemx --version
+@opindex -V@r{, @code{printf_gettext} option}
+@opindex --version@r{, @code{printf_gettext} option}
+Output version information and exit.
+
+@end table
+
+The format string consists of
+@itemize @bullet
+@item
+plain text,
+@item
+directives, that start with @samp{%},
+@item
+escape sequences, that start with a backslash.
+@end itemize
+
+A directive that consumes an argument
+@itemize @bullet
+@item
+starts with @samp{%} or @samp{%@var{m}$} where @var{m} is a positive integer,
+@item
+is optionally followed by any of the characters
+@samp{#}, @samp{0}, @samp{-}, @samp{ }, @samp{+},
+each of which acts as a flag,
+@item
+is optionally followed by a width specification (a nonnegative integer),
+@item
+is optionally followed by @samp{.} and a precision specification
+(an optional nonnegative integer),
+@item
+is finished by a specifier
+@itemize @bullet
+@item
+@samp{c}, that prints a character,
+@item
+@samp{s}, that prints a string,
+@item
+@samp{i}, @samp{d}, that print an integer,
+@item
+@samp{u}, @samp{o}, @samp{x}, @samp{X},
+that print an unsigned (nonnegative) integer,
+@item
+@samp{e}, @samp{E}, that print a floating-point number in scientific notation,
+@item
+@samp{f}, @samp{F}, that print a floating-point number without an exponent,
+@item
+@samp{g}, @samp{G}, that print a floating-point number in general notation,
+@item
+@samp{a}, @samp{A}, that print a floating-point number in hexadecimal notation.
+@end itemize
+@end itemize
+
+Some flag+specifier combinations are invalid:
+@itemize @bullet
+@item
+The @samp{#} flag with the specifiers
+@samp{c}, @samp{s}, @samp{i}, @samp{d}, @samp{u}.
+@item
+The @samp{0} flag with the specifiers
+@samp{c}, @samp{s}.
+@end itemize
+
+Additionally there is the directive @samp{%%}, that prints a single @code{%}.
+
+If a directive specifies the argument by its number (@samp{%@var{m}$} notation),
+all directives that consume an argument must do so.
+
+The escape sequences are:
+@table @code
+@item \\
+backslash
+@item \a
+alert (BEL)
+@item \b
+backspace (BS)
+@item \f
+form feed (FF)
+@item \n
+new line (LF)
+@item \r
+carriage return (CR)
+@item \t
+horizontal tab (HT)
+@item \v
+vertical tab (VT)
+@item \@var{nnn}
+octal number with 1 to 3 octal digits
+@end table
+
+@noindent @strong{Environment Variables}
+
+The translation of the format string is looked up in the translation domain
+given by the environment variable @code{TEXTDOMAIN}.
+
+It is looked up in the catalogs directory given by the environment variable
+@code{TEXTDOMAINDIR} or, if not present, in the default catalogs directory.
diff --git a/gettext-runtime/doc/rt-printf_ngettext.texi b/gettext-runtime/doc/rt-printf_ngettext.texi
new file mode 100644 (file)
index 0000000..59d540e
--- /dev/null
@@ -0,0 +1,149 @@
+@c This file is part of the GNU gettext manual.
+@c Copyright (C) 2025 Free Software Foundation, Inc.
+@c See the file gettext.texi for copying conditions.
+
+@pindex printf_ngettext
+@cindex @code{printf_ngettext} program, usage
+@example
+printf_ngettext [@var{option}] @var{format} @var{format-plural} @var{count} [@var{argument}]...
+@end example
+
+@cindex lookup format string translation with plural
+@cindex formatted output in Shell
+The @code{printf_ngettext} program produces formatted output,
+applying the native language translation of
+@var{format} and @var{format-plural}, depending on @var{count},
+to the @var{argument}s.
+
+@noindent @strong{Options and arguments}
+
+@table @samp
+@item -c @var{context}
+@itemx --context=@var{context}
+@opindex -c@r{, @code{printf_ngettext} option}
+@opindex --context@r{, @code{printf_ngettext} option}
+Specify the context for the format string to be translated.
+See @ref{Contexts} for details.
+
+@item @var{format}
+English singular form of format string.
+
+@item @var{format-plural}
+English plural form of format string.
+
+@item @var{count}
+A cardinal number.
+The singular/plural form is chosen based on this value.
+
+@item @var{argument}
+A string or numeric argument.
+
+@end table
+
+@noindent @strong{Informative output}
+
+@table @samp
+@item -h
+@itemx --help
+@opindex -h@r{, @code{printf_ngettext} option}
+@opindex --help@r{, @code{printf_ngettext} option}
+Display this help and exit.
+
+@item -V
+@itemx --version
+@opindex -V@r{, @code{printf_ngettext} option}
+@opindex --version@r{, @code{printf_ngettext} option}
+Output version information and exit.
+
+@end table
+
+Each format string consists of
+@itemize @bullet
+@item
+plain text,
+@item
+directives, that start with @samp{%},
+@item
+escape sequences, that start with a backslash.
+@end itemize
+
+A directive that consumes an argument
+@itemize @bullet
+@item
+starts with @samp{%} or @samp{%@var{m}$} where @var{m} is a positive integer,
+@item
+is optionally followed by any of the characters
+@samp{#}, @samp{0}, @samp{-}, @samp{ }, @samp{+},
+each of which acts as a flag,
+@item
+is optionally followed by a width specification (a nonnegative integer),
+@item
+is optionally followed by @samp{.} and a precision specification
+(an optional nonnegative integer),
+@item
+is finished by a specifier
+@itemize @bullet
+@item
+@samp{c}, that prints a character,
+@item
+@samp{s}, that prints a string,
+@item
+@samp{i}, @samp{d}, that print an integer,
+@item
+@samp{u}, @samp{o}, @samp{x}, @samp{X},
+that print an unsigned (nonnegative) integer,
+@item
+@samp{e}, @samp{E}, that print a floating-point number in scientific notation,
+@item
+@samp{f}, @samp{F}, that print a floating-point number without an exponent,
+@item
+@samp{g}, @samp{G}, that print a floating-point number in general notation,
+@item
+@samp{a}, @samp{A}, that print a floating-point number in hexadecimal notation.
+@end itemize
+@end itemize
+
+Some flag+specifier combinations are invalid:
+@itemize @bullet
+@item
+The @samp{#} flag with the specifiers
+@samp{c}, @samp{s}, @samp{i}, @samp{d}, @samp{u}.
+@item
+The @samp{0} flag with the specifiers
+@samp{c}, @samp{s}.
+@end itemize
+
+Additionally there is the directive @samp{%%}, that prints a single @code{%}.
+
+If a directive specifies the argument by its number (@samp{%@var{m}$} notation),
+all directives that consume an argument must do so.
+
+The escape sequences are:
+@table @code
+@item \\
+backslash
+@item \a
+alert (BEL)
+@item \b
+backspace (BS)
+@item \f
+form feed (FF)
+@item \n
+new line (LF)
+@item \r
+carriage return (CR)
+@item \t
+horizontal tab (HT)
+@item \v
+vertical tab (VT)
+@item \@var{nnn}
+octal number with 1 to 3 octal digits
+@end table
+
+@noindent @strong{Environment Variables}
+
+The translation of the format string is looked up in the translation domain
+given by the environment variable @code{TEXTDOMAIN}.
+
+It is looked up in the catalogs directory given by the environment variable
+@code{TEXTDOMAINDIR} or, if not present, in the default catalogs directory.
index 9e6f85f22caf640864bef687537c985a8401b191..3273157ef1932ca767c7d7c26b5173c0d76e949a 100644 (file)
@@ -1,6 +1,5 @@
 ## Makefile for the gettext-runtime/man subdirectory of GNU gettext
-## Copyright (C) 2001-2003, 2006, 2009, 2013-2014, 2018-2019 Free Software Foundation,
-## Inc.
+## Copyright (C) 2001-2025 Free Software Foundation, Inc.
 ##
 ## This program is free software: you can redistribute it and/or modify
 ## it under the terms of the GNU General Public License as published by
@@ -24,12 +23,12 @@ EXTRA_DIST =
 
 # A manual page for each of the bin_PROGRAMS in src/Makefile.am.
 
-man_aux = gettext.x ngettext.x envsubst.x
+man_aux = gettext.x ngettext.x printf_gettext.x printf_ngettext.x envsubst.x
 
 # Likewise, plus additional manual pages for the libintl functions.
 
-man_MAN1GEN = gettext.1 ngettext.1
-man_MAN1IN = gettext.1.in ngettext.1.in
+man_MAN1GEN = gettext.1 ngettext.1 printf_gettext.1 printf_ngettext.1
+man_MAN1IN = gettext.1.in ngettext.1.in printf_gettext.1.in printf_ngettext.1.in
 man_MAN1OTHER = envsubst.1
 man_MAN1 = $(man_MAN1GEN) $(man_MAN1OTHER)
 man_MAN3 = gettext.3 ngettext.3 \
@@ -40,8 +39,8 @@ man_MAN3LINK = dgettext.3 dcgettext.3 dngettext.3 dcngettext.3
 man_MANS = $(man_MAN1)
 notrans_man_MANS = $(man_MAN3) $(man_MAN3LINK)
 
-man_HTML1GEN = gettext.1.html ngettext.1.html
-man_HTML1IN = gettext.1.html.in ngettext.1.html.in
+man_HTML1GEN = gettext.1.html ngettext.1.html printf_gettext.1.html printf_ngettext.1.html
+man_HTML1IN = gettext.1.html.in ngettext.1.html.in printf_gettext.1.html.in printf_ngettext.1.html.in
 man_HTML1OTHER = envsubst.1.html
 man_HTML1 = $(man_HTML1GEN) $(man_HTML1OTHER)
 man_HTML3 = gettext.3.html ngettext.3.html \
@@ -108,6 +107,8 @@ $(man_MAN1GEN): Makefile
 
 gettext.1: gettext.1.in
 ngettext.1: ngettext.1.in
+printf_gettext.1: printf_gettext.1.in
+printf_ngettext.1: printf_ngettext.1.in
 
 $(man_MAN1IN) $(man_MAN1OTHER): help2man $(top_srcdir)/../.version
        progname=`echo $@ | sed -e 's/\.in$$//' -e 's/\.1$$//'`; \
@@ -116,6 +117,8 @@ $(man_MAN1IN) $(man_MAN1OTHER): help2man $(top_srcdir)/../.version
 
 gettext.1.in: gettext.x ../src/gettext.c
 ngettext.1.in: ngettext.x ../src/ngettext.c
+printf_gettext.1.in: printf_gettext.x ../src/printf_gettext.c
+printf_ngettext.1.in: printf_ngettext.x ../src/printf_ngettext.c
 envsubst.1: envsubst.x ../src/envsubst.c
 
 $(man_MAN3): $(top_srcdir)/../.version
@@ -143,6 +146,8 @@ $(man_HTML1GEN): Makefile
 
 gettext.1.html: gettext.1.html.in
 ngettext.1.html: ngettext.1.html.in
+printf_gettext.1.html: printf_gettext.1.html.in
+printf_ngettext.1.html: printf_ngettext.1.html.in
 
 $(man_HTML1IN):
        srcdir=''; \
@@ -153,6 +158,8 @@ $(man_HTML1IN):
 
 gettext.1.html.in: gettext.1.in
 ngettext.1.html.in: ngettext.1.in
+printf_gettext.1.html.in: printf_gettext.1.in
+printf_ngettext.1.html.in: printf_ngettext.1.in
 
 $(man_HTML1OTHER):
        srcdir=''; \
diff --git a/gettext-runtime/man/printf_gettext.x b/gettext-runtime/man/printf_gettext.x
new file mode 100644 (file)
index 0000000..c8eb2bd
--- /dev/null
@@ -0,0 +1,7 @@
+[NAME]
+printf_gettext \- translate format string and apply it
+[DESCRIPTION]
+.\" Add any additional description here
+The \fBprintf_gettext\fP program translates a format string into the user's
+language, by looking up the translation in a message catalog, and applies
+the translated format string to the specified arguments.
diff --git a/gettext-runtime/man/printf_ngettext.x b/gettext-runtime/man/printf_ngettext.x
new file mode 100644 (file)
index 0000000..0047726
--- /dev/null
@@ -0,0 +1,9 @@
+[NAME]
+printf_ngettext \- translate format string and apply it
+[DESCRIPTION]
+.\" Add any additional description here
+The \fBprintf_ngettext\fP program translates a format string into the user's
+language, by looking up the translation in a message catalog and then
+choosing the appropriate plural form, which depends on the number \fICOUNT\fP
+and the language of the message catalog where the translation was found, and
+applies the translated format string to the specified arguments.
index 791de449af5b218c0ed1c6de0d963de1a8f151ff..a86066f772929ead63be5a62d19bd79ae0ccb1b9 100644 (file)
@@ -1,5 +1,5 @@
 # List of files which contain translatable strings.
-# Copyright (C) 1995-2024 Free Software Foundation, Inc.
+# Copyright (C) 1995-2025 Free Software Foundation, Inc.
 # This file is free software, distributed under GNU GPL v3+.
 
 # For updating this file, look at the result of:
@@ -9,3 +9,6 @@
 src/envsubst.c
 src/gettext.c
 src/ngettext.c
+src/printf-command.c
+src/printf_gettext.c
+src/printf_ngettext.c
index bb6e569e2917f65413302caaeeffb42555f299e6..bec7e47de8f93c38fb267a966edcb50fb24cb3ab 100644 (file)
@@ -1,5 +1,5 @@
 ## Makefile for the gettext-runtime/src subdirectory of GNU gettext
-## Copyright (C) 1995-2024 Free Software Foundation, Inc.
+## Copyright (C) 1995-2025 Free Software Foundation, Inc.
 ##
 ## This program is free software: you can redistribute it and/or modify
 ## it under the terms of the GNU General Public License as published by
@@ -24,7 +24,9 @@ DISTCLEANFILES =
 
 RM = rm -f
 
-bin_PROGRAMS = gettext ngettext envsubst
+bin_PROGRAMS = gettext ngettext printf_gettext printf_ngettext envsubst
+
+noinst_LIBRARIES = libgrtsrc.a
 
 # Note that Automake's $(DEFAULT_INCLUDES) already contains
 # -I. -I$(srcdir) -I$(top_builddir).
@@ -39,19 +41,29 @@ AM_CFLAGS = @WARN_CFLAGS@
 # Source dependencies.
 gettext_SOURCES = gettext.c escapes.h
 ngettext_SOURCES = ngettext.c escapes.h
+printf_gettext_SOURCES = printf_gettext.c
+printf_ngettext_SOURCES = printf_ngettext.c
 envsubst_SOURCES = envsubst.c
 
+# libgrtsrc contains all code that is needed by at least two programs.
+libgrtsrc_a_SOURCES = \
+  printf-command.h printf-command.c
+
 # Link dependencies.
 # Need @LTLIBICONV@ because striconv.c uses iconv().
-LDADD = ../gnulib-lib/libgrt.a @LTLIBINTL@ @LTLIBICONV@ $(WOE32_LDADD)
+LDADD = libgrtsrc.a ../gnulib-lib/libgrt.a @LTLIBINTL@ @LTLIBICONV@ $(WOE32_LDADD)
 
 # Specify installation directory, for --enable-relocatable.
 gettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make)
 ngettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make)
+printf_gettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make)
+printf_ngettext_CFLAGS = -DINSTALLDIR=$(bindir_c_make)
 envsubst_CFLAGS = -DINSTALLDIR=$(bindir_c_make)
 if RELOCATABLE_VIA_LD
 gettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)`
 ngettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)`
+printf_gettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)`
+printf_ngettext_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)`
 envsubst_LDFLAGS = `$(RELOCATABLE_LDFLAGS) $(bindir)`
 endif
 
diff --git a/gettext-runtime/src/printf-command.c b/gettext-runtime/src/printf-command.c
new file mode 100644 (file)
index 0000000..92767bd
--- /dev/null
@@ -0,0 +1,744 @@
+/* Formatted output with a POSIX compatible format string.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "printf-command.h"
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <uchar.h>
+
+#include <error.h>
+#include "attribute.h"
+#include "c-ctype.h"
+#include "strnlen1.h"
+#include "c-strtod.h"
+#include "xstrtod.h"
+#include "quote.h"
+#include "xalloc.h"
+#include "gettext.h"
+
+#define _(str) gettext (str)
+
+/* The argument type consumed by a directive.  */
+enum format_arg_type
+{
+  FAT_CHARACTER,
+  FAT_STRING,
+  FAT_INTEGER,
+  FAT_UNSIGNED_INTEGER,
+  FAT_FLOAT
+};
+
+/* A piece of output.  */
+struct format_piece
+{
+  /* For plain text, directives that take no argument, and escape sequences:  */
+  const char *text_start;
+  size_t text_length;
+  /* For directives that take an argument:  */
+  enum format_arg_type arg_type;
+  size_t arg_number; /* > 0 */
+  const char *arg_fmt;
+};
+
+/* The entire format string.  */
+struct format_string
+{
+  struct format_piece *pieces;
+  size_t npieces;
+};
+
+/* Parses the format string.
+   Returns the number of arguments that it consumes.  */
+static size_t
+parse_format_string (struct format_string *fmts, const char *format)
+{
+  struct format_piece *pieces = NULL;
+  size_t npieces = 0;
+  size_t npieces_allocated = 0;
+
+  size_t directives = 0;
+  size_t numbered_arg_count = 0;
+  size_t unnumbered_arg_count = 0;
+  size_t max_numbered_arg = 0;
+  const char *current_piece_start = NULL;
+
+  for (;;)
+    {
+      /* Invariant: numbered_arg_count == 0 || unnumbered_arg_count == 0.  */
+      /* Invariant: current_piece_start == NULL || current_piece_start < format.  */
+      if (*format == '\0' || *format == '%' || *format == '\\')
+        {
+          if (current_piece_start != NULL)
+            {
+              if (npieces == npieces_allocated)
+                {
+                  npieces_allocated = 2 * npieces_allocated + 1;
+                  pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece));
+                }
+              pieces[npieces].text_start = current_piece_start;
+              pieces[npieces].text_length = format - current_piece_start;
+              npieces++;
+              current_piece_start = NULL;
+            }
+        }
+      else
+        {
+          if (current_piece_start == NULL)
+            current_piece_start = format;
+        }
+
+      if (*format == '\0')
+        break;
+
+      if (*format == '%')
+        {
+          /* A directive.  */
+          format++;
+          directives++;
+
+          if (*format == '%')
+            {
+              /* "%%" produces a literal '%'.  */
+              if (npieces == npieces_allocated)
+                {
+                  npieces_allocated = 2 * npieces_allocated + 1;
+                  pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece));
+                }
+              pieces[npieces].text_start = "%";
+              pieces[npieces].text_length = 1;
+              npieces++;
+            }
+          else
+            {
+              size_t number = 0;
+              if (c_isdigit (*format))
+                {
+                  const char *f = format;
+                  size_t m = 0;
+
+                  do
+                    {
+                      m = 10 * m + (*f - '0');
+                      f++;
+                    }
+                  while (c_isdigit (*f));
+
+                  if (*f == '$')
+                    {
+                      if (m == 0)
+                        error (EXIT_FAILURE, 0,
+                               _("In the directive number %zu, the argument number 0 is not a positive integer."),
+                               directives);
+                      number = m;
+                      format = ++f;
+                    }
+                }
+
+              /* Parse flags.  */
+              bool have_space_flag = false;
+              bool have_plus_flag = false;
+              bool have_minus_flag = false;
+              bool have_hash_flag = false;
+              bool have_zero_flag = false;
+              for (;; format++)
+                {
+                  switch (*format)
+                    {
+                    case ' ':
+                      have_space_flag = true;
+                      continue;
+                    case '+':
+                      have_plus_flag = true;
+                      continue;
+                    case '-':
+                      have_minus_flag = true;
+                      continue;
+                    case '#':
+                      have_hash_flag = true;
+                      continue;
+                    case '0':
+                      have_zero_flag = true;
+                      continue;
+                    default:
+                      break;
+                    }
+                  break;
+                }
+
+              /* Parse width.  */
+              const char *width_start = NULL;
+              size_t width_length = 0;
+              if (c_isdigit (*format))
+                {
+                  width_start = format;
+                  do format++; while (c_isdigit (*format));
+                  width_length = format - width_start;
+                }
+
+              /* Parse precision.  */
+              const char *precision_start = NULL;
+              size_t precision_length = 0;
+              if (*format == '.')
+                {
+                  format++;
+
+                  precision_start = format;
+                  while (c_isdigit (*format))
+                    format++;
+                  precision_length = format - precision_start;
+                }
+
+              enum format_arg_type type;
+              switch (*format)
+                {
+                case 'c':
+                  type = FAT_CHARACTER;
+                  break;
+                case 's':
+                  type = FAT_STRING;
+                  break;
+                case 'i': case 'd':
+                  type = FAT_INTEGER;
+                  break;
+                case 'u': case 'o': case 'x': case 'X':
+                  type = FAT_UNSIGNED_INTEGER;
+                  break;
+                case 'e': case 'E': case 'f': case 'F': case 'g': case 'G':
+                case 'a': case 'A':
+                  type = FAT_FLOAT;
+                  break;
+                default:
+                  if (*format == '\0')
+                    error (EXIT_FAILURE, 0,
+                           _("The string ends in the middle of a directive."));
+                  else
+                    {
+                      if (c_isprint (*format))
+                        error (EXIT_FAILURE, 0,
+                               _("In the directive number %zu, the character '%c' is not a valid conversion specifier."),
+                               directives, *format);
+                      else
+                        error (EXIT_FAILURE, 0,
+                               _("The character that terminates the directive number %zu is not a valid conversion specifier."),
+                               directives);
+                   }
+                }
+
+              if (have_hash_flag
+                  && (*format == 'c' || *format == 's'
+                      || *format == 'i' || *format == 'd' || *format == 'u'))
+                error (EXIT_FAILURE, 0,
+                       _("In the directive number %zu, the flag '%c' is invalid for the conversion '%c'."),
+                       directives, '#', *format);
+              if (have_zero_flag && (*format == 'c' || *format == 's'))
+                error (EXIT_FAILURE, 0,
+                       _("In the directive number %zu, the flag '%c' is invalid for the conversion '%c'."),
+                       directives, '0', *format);
+
+              if (npieces == npieces_allocated)
+                {
+                  npieces_allocated = 2 * npieces_allocated + 1;
+                  pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece));
+                }
+              pieces[npieces].text_start = NULL;
+              pieces[npieces].text_length = 0;
+              pieces[npieces].arg_type = type;
+
+              if (number)
+                {
+                  /* Numbered argument.  */
+
+                  /* Numbered and unnumbered specifications are exclusive.  */
+                  if (unnumbered_arg_count > 0)
+                    error (EXIT_FAILURE, 0,
+                           _("The string refers to arguments both through absolute argument numbers and through unnumbered argument specifications."));
+
+                  pieces[npieces].arg_number = number;
+                  numbered_arg_count++;
+                  if (max_numbered_arg < number)
+                    max_numbered_arg = number;
+                }
+              else
+                {
+                  /* Unnumbered argument.  */
+
+                  /* Numbered and unnumbered specifications are exclusive.  */
+                  if (numbered_arg_count > 0)
+                    error (EXIT_FAILURE, 0,
+                           _("The string refers to arguments both through absolute argument numbers and through unnumbered argument specifications."));
+
+                  pieces[npieces].arg_number = unnumbered_arg_count + 1;
+                  unnumbered_arg_count++;
+                }
+
+              if (fmts != NULL)
+                {
+                  char *arg_fmt = (char *) xmalloc (1 + 5 + width_length + 1 + precision_length + 2 + 1);
+                  {
+                    char *f = arg_fmt;
+                    *f++ = '%';
+                    if (have_space_flag)
+                      *f++ = ' ';
+                    if (have_plus_flag)
+                      *f++ = '+';
+                    if (have_minus_flag)
+                      *f++ = '-';
+                    if (have_hash_flag)
+                      *f++ = '#';
+                    if (have_zero_flag)
+                      *f++ = '0';
+                    if (width_start != NULL)
+                      {
+                        memcpy (f, width_start, width_length);
+                        f += width_length;
+                      }
+                    if (precision_start != NULL)
+                      {
+                        *f++ = '.';
+                        memcpy (f, precision_start, precision_length);
+                        f += precision_length;
+                      }
+                    switch (type)
+                      {
+                      case FAT_INTEGER:
+                      case FAT_UNSIGNED_INTEGER:
+                        *f++ = 'j';
+                        break;
+                      case FAT_FLOAT:
+                        *f++ = 'L';
+                        break;
+                      default:
+                        break;
+                      }
+                    *f++ = (*format == 'c' ? 's' : *format);
+                    *f = '\0';
+                  }
+                  pieces[npieces].arg_fmt = arg_fmt;
+                }
+
+              npieces++;
+            }
+
+          format++;
+        }
+      else if (*format == '\\')
+        {
+          /* An escape sequence.  */
+          format++;
+
+          const char *one_char;
+          switch (*format)
+            {
+            case '\\': one_char = "\\"; format++; break;
+            case 'a':  one_char = "\a"; format++; break;
+            case 'b':  one_char = "\b"; format++; break;
+            case 'f':  one_char = "\f"; format++; break;
+            case 'n':  one_char = "\n"; format++; break;
+            case 'r':  one_char = "\r"; format++; break;
+            case 't':  one_char = "\t"; format++; break;
+            case 'v':  one_char = "\v"; format++; break;
+
+            case '0': case '1': case '2': case '3': case '4': case '5':
+            case '6': case '7':
+              {
+                unsigned int n = (*format - '0');
+                format++;
+                if (*format >= '0' && *format <= '7')
+                  {
+                    n =  (n << 3) + (*format - '0');
+                    format++;
+                    if (*format >= '0' && *format <= '7')
+                      {
+                        n =  (n << 3) + (*format - '0');
+                        format++;
+                      }
+                  }
+                if (fmts != NULL)
+                  {
+                    char *text = (char *) xmalloc (1);
+                    *text = (unsigned char) n;
+                    one_char = text;
+                  }
+                else
+                  one_char = ""; /* just a dummy */
+              }
+              break;
+
+            default:
+              if (*format == '\0')
+                error (EXIT_FAILURE, 0,
+                       _("The string ends in the middle of an escape sequence."));
+              else
+                {
+                  if (c_isprint (*format))
+                    error (EXIT_FAILURE, 0,
+                           (*format == 'c'
+                            || *format == 'x'
+                            || *format == 'u' || *format == 'U'
+                            ? _("The escape sequence '%c%c' is unsupported (not in POSIX).")
+                            : _("The escape sequence '%c%c' is invalid.")),
+                           '\\', *format);
+                  else
+                    error (EXIT_FAILURE, 0,
+                           _("This escape sequence is invalid."));
+                }
+            }
+
+          if (npieces == npieces_allocated)
+            {
+              npieces_allocated = 2 * npieces_allocated + 1;
+              pieces = (struct format_piece *) xrealloc (pieces, npieces_allocated * sizeof (struct format_piece));
+            }
+          pieces[npieces].text_start = one_char;
+          pieces[npieces].text_length = 1;
+          npieces++;
+        }
+      else
+        format++;
+    }
+
+  if (fmts != NULL)
+    {
+      fmts->pieces = pieces;
+      fmts->npieces = npieces;
+    }
+  else
+    free (pieces);
+
+  /* The number of consumed arguments:  */
+  return (numbered_arg_count > 0 ? max_numbered_arg : unnumbered_arg_count);
+}
+
+static int status;
+
+/* Applies the format string to the array of remaining arguments.  */
+static void
+apply_format_string (const struct format_string *fmts,
+                     size_t argc, char *argv[])
+{
+  size_t npieces = fmts->npieces;
+  size_t i;
+
+  for (i = 0; i < npieces; i++)
+    {
+      struct format_piece *piece = &fmts->pieces[i];
+
+      if (piece->text_start != NULL)
+        {
+          /* Print some fixed text.  */
+          if (fwrite (piece->text_start, 1, piece->text_length, stdout)
+              < piece->text_length)
+            error (EXIT_FAILURE, 0, _("write error"));
+        }
+      else
+        {
+          /* Convert and print an argument.  */
+          char *arg;
+          char zero[2] = { '0', '\0' };
+          char *empty = zero + 1;
+
+          if (piece->arg_number - 1 < argc)
+            arg = argv[piece->arg_number - 1];
+          else
+            {
+              /* <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+                 point 11 suggests that we make "%1$x" behave differently from
+                 "%x".  We don't do this, because translators are free to switch
+                 from unnumbered arguments to numbered arguments or vice versa.  */
+              arg = (piece->arg_type == FAT_CHARACTER
+                     || piece->arg_type == FAT_STRING
+                     ? empty
+                     : zero);
+            }
+
+          switch (piece->arg_type)
+            {
+            case FAT_CHARACTER:
+              /* <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+                 point 13 suggests to print the first *byte* of arg.  But this
+                 is not appropriate in multibyte locales.   Therefore, print the
+                 first multibyte character instead, if arg starts with a valid
+                 multibyte character.  */
+              {
+                mbstate_t state;
+                char32_t wc;
+
+                mbszero (&state);
+                size_t ret = mbrtoc32 (&wc, arg, strnlen1 (arg, MB_CUR_MAX), &state);
+                arg[(int) ret >= 0 ? ret : 1] = '\0';
+              }
+              FALLTHROUGH;
+            case FAT_STRING:
+              errno = 0;
+              if (fzprintf (stdout, piece->arg_fmt, arg) < 0)
+                {
+                  if (errno == ENOMEM)
+                    xalloc_die ();
+                  error (EXIT_FAILURE, 0, _("write error"));
+                }
+              break;
+
+            case FAT_INTEGER:
+              {
+                intmax_t arg_value;
+                if (*arg == '\'' || *arg == '"')
+                  {
+                    /* POSIX says: "If the leading character is a single-quote
+                       or double-quote, the value shall be the numeric value
+                       in the underlying codeset of the character following the
+                       single-quote or double-quote."
+                       Use the first first multibyte character, if arg starts
+                       with a valid multibyte character.  */
+                    mbstate_t state;
+                    char32_t wc;
+
+                    mbszero (&state);
+                    size_t ret = mbrtoc32 (&wc, arg + 1, strnlen1 (arg + 1, MB_CUR_MAX), &state);
+                    if ((int) ret > 0)
+                      arg_value = wc;
+                    else if (arg[1] != '\0')
+                      arg_value = (unsigned char) arg[1];
+                    else
+                      {
+                        arg_value = 0;
+                        error (EXIT_SUCCESS, 0,
+                               _("%s: expected a numeric value"),
+                               quote (arg));
+                        status = EXIT_FAILURE;
+                      }
+                  }
+                else
+                  {
+                    /* xstrtoimax is a nicer API than strtoimax.
+                       Let's hope that I don't make a mistake with strtoimax's
+                       horrible calling convention here.  */
+                    char *ptr;
+                    arg_value = (errno = 0, strtoimax (arg, &ptr, 0));
+                    bool parsed = (ptr != arg && errno == 0);
+
+                    if (parsed && *ptr == '\0')
+                      /* Successful parse of arg.  */
+                      ;
+                    else
+                      {
+                        if (parsed)
+                          error (EXIT_SUCCESS, 0,
+                                 _("%s: value not completely converted"),
+                                 quote (arg));
+                        else
+                          {
+                            arg_value = 0;
+                            error (EXIT_SUCCESS, 0,
+                                   _("%s: expected a numeric value"),
+                                   quote (arg));
+                          }
+                        status = EXIT_FAILURE;
+                      }
+                  }
+
+                errno = 0;
+                if (fzprintf (stdout, piece->arg_fmt, arg_value) < 0)
+                  {
+                    if (errno == ENOMEM)
+                      xalloc_die ();
+                    error (EXIT_FAILURE, 0, _("write error"));
+                  }
+              }
+              break;
+
+            case FAT_UNSIGNED_INTEGER:
+              {
+                uintmax_t arg_value;
+                if (*arg == '\'' || *arg == '"')
+                  {
+                    /* POSIX says: "If the leading character is a single-quote
+                       or double-quote, the value shall be the numeric value
+                       in the underlying codeset of the character following the
+                       single-quote or double-quote."
+                       Use the first first multibyte character, if arg starts
+                       with a valid multibyte character.  */
+                    mbstate_t state;
+                    char32_t wc;
+
+                    mbszero (&state);
+                    size_t ret = mbrtoc32 (&wc, arg + 1, strnlen1 (arg + 1, MB_CUR_MAX), &state);
+                    if ((int) ret > 0)
+                      arg_value = wc;
+                    else if (arg[1] != '\0')
+                      arg_value = (unsigned char) arg[1];
+                    else
+                      {
+                        arg_value = 0;
+                        error (EXIT_SUCCESS, 0,
+                               _("%s: expected a numeric value"),
+                               quote (arg));
+                        status = EXIT_FAILURE;
+                      }
+                  }
+                else
+                  {
+                    /* xstrtoumax is a nicer API than strtoumax.
+                       But here, we need to accept a leading '-' sign, as in
+                       "-3" or " -3".
+                       Let's hope that I don't make a mistake with strtoumax's
+                       horrible calling convention here.  */
+                    char *ptr;
+                    arg_value = (errno = 0, strtoumax (arg, &ptr, 0));
+                    bool parsed = (ptr != arg && errno == 0);
+
+                    if (parsed && *ptr == '\0')
+                      /* Successful parse of arg.  */
+                      ;
+                    else
+                      {
+                        if (parsed)
+                          error (EXIT_SUCCESS, 0,
+                                 _("%s: value not completely converted"),
+                                 quote (arg));
+                        else
+                          {
+                            arg_value = 0;
+                            error (EXIT_SUCCESS, 0,
+                                   _("%s: expected a numeric value"),
+                                   quote (arg));
+                          }
+                        status = EXIT_FAILURE;
+                      }
+                  }
+
+                errno = 0;
+                if (fzprintf (stdout, piece->arg_fmt, arg_value) < 0)
+                  {
+                    if (errno == ENOMEM)
+                      xalloc_die ();
+                    error (EXIT_FAILURE, 0, _("write error"));
+                  }
+              }
+              break;
+
+            case FAT_FLOAT:
+              /* <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+                 suggests to use strtod(), i.e. a 'double'.  We prefer a
+                 'long double', because it has higher precision.  */
+              /* Try interpreting the argument as a number in the current locale
+                 and, if that fails, in the "C" locale.  Like coreutils 'printf'
+                 does.  */
+              {
+                long double arg_value;
+                const char *ptr;
+                bool parsed = xstrtold (arg, &ptr, &arg_value, strtold);
+                if (parsed && *ptr == '\0')
+                  /* Successful parse of arg in the current locale.  */
+                  ;
+                else
+                  {
+                    long double arg_value2;
+                    const char *ptr2;
+                    bool parsed2 = xstrtold (arg, &ptr2, &arg_value2, c_strtold);
+                    if (parsed2 && *ptr2 == '\0')
+                      {
+                        /* Successful parse of arg in the "C" locale.  */
+                        arg_value = arg_value2;
+                      }
+                    else
+                      {
+                        if (parsed2 && (!parsed || ptr2 > ptr))
+                          arg_value = arg_value2;
+                        if (parsed || parsed2)
+                          error (EXIT_SUCCESS, 0,
+                                 _("%s: value not completely converted"),
+                                 quote (arg));
+                        else
+                          {
+                            arg_value = 0.0L;
+                            error (EXIT_SUCCESS, 0,
+                                   _("%s: expected a numeric value"),
+                                   quote (arg));
+                          }
+                        status = EXIT_FAILURE;
+                      }
+                  }
+
+                errno = 0;
+                if (fzprintf (stdout, piece->arg_fmt, arg_value) < 0)
+                  {
+                    if (errno == ENOMEM)
+                      xalloc_die ();
+                    error (EXIT_FAILURE, 0, _("write error"));
+                  }
+              }
+              break;
+            }
+        }
+    }
+}
+
+size_t
+printf_consumed_arguments (const char *format)
+{
+  return parse_format_string (NULL, format);
+}
+
+void
+printf_command (const char *format, size_t args_each_round,
+                size_t argc, char *argv[])
+{
+  /* Parse the format string, and bail out early if it is invalid.  */
+  struct format_string fmts;
+  size_t consumed_arguments = parse_format_string (&fmts, format);
+
+  /* Validate consumed_arguments against args_each_round.  */
+  if (consumed_arguments > args_each_round)
+    error (EXIT_FAILURE, 0,
+           _("The translated format string consumes %zu arguments, whereas the original format string consumes only %zu arguments."),
+           consumed_arguments, args_each_round);
+  /* Here consumed_arguments <= args_each_round.
+     It is OK if consumed_arguments < args_each_round; this happens for example
+     in 'printf_ngettext', when the chosen format string applies only to a
+     single value.  */
+
+  /* Repeatedly apply the format string to the remaining arguments.  */
+  if (args_each_round == 0 && argc > 0)
+    {
+      error (0, 0,
+             _("warning: ignoring excess arguments, starting with %s"),
+             quote(argv[0]));
+      argc = 0;
+    }
+  status = EXIT_SUCCESS;
+  for (;;)
+    {
+      apply_format_string (&fmts, argc, argv);
+      if (argc <= args_each_round)
+        break;
+      argc -= args_each_round;
+      argv += args_each_round;
+    }
+
+  if (status != EXIT_SUCCESS)
+    exit (status);
+}
diff --git a/gettext-runtime/src/printf-command.h b/gettext-runtime/src/printf-command.h
new file mode 100644 (file)
index 0000000..ac66a72
--- /dev/null
@@ -0,0 +1,46 @@
+/* Formatted output with a POSIX compatible format string.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025.  */
+
+/* This file implements the bulk of the POSIX:2024 specification for the 'printf'
+   command:
+   <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html>
+   <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap05.html#tag_05>
+   including the floating-point conversion specifiers 'a', 'A', 'e', 'E',
+   'f', 'F', 'g', 'G', but without the obsolescent 'b' conversion specifier.  */
+
+#ifndef _PRINTF_COMMAND_H
+#define _PRINTF_COMMAND_H
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Returns the number of arguments that a format string consumes.  */
+extern size_t printf_consumed_arguments (const char *format);
+
+/* Applies a format string to a sequence of string arguments.  */
+extern void printf_command (const char *format, size_t args_each_round,
+                            size_t argc, char *argv[]);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _PRINTF_COMMAND_H */
diff --git a/gettext-runtime/src/printf_gettext.c b/gettext-runtime/src/printf_gettext.c
new file mode 100644 (file)
index 0000000..e92e06a
--- /dev/null
@@ -0,0 +1,287 @@
+/* Formatted output with a localized format string.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025.  */
+
+/* This program is a combination of the 'gettext' program with the 'printf'
+   program.  It takes a format string and arguments, looks up the translation
+   of the format string (for the current locale, according to the environment
+   variables TEXTDOMAIN and TEXTDOMAINDIR), and applies that translated format
+   string to the arguments.  */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <getopt.h>
+#include <locale.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <error.h>
+#include "printf-command.h"
+#include "noreturn.h"
+#include "closeout.h"
+#include "progname.h"
+#include "relocatable.h"
+#include "basename-lgpl.h"
+#include "propername.h"
+#include "gettext.h"
+
+#define _(str) gettext (str)
+
+/* Forward declaration of local functions.  */
+_GL_NORETURN_FUNC static void usage (int status);
+
+int
+main (int argc, char *argv[])
+{
+  /* Default values for command line options.  */
+  bool do_help = false;
+  bool do_version = false;
+  const char *domain = getenv ("TEXTDOMAIN");
+  const char *domaindir = getenv ("TEXTDOMAINDIR");
+  const char *context = NULL;
+
+  /* Set program name for message texts.  */
+  set_program_name (argv[0]);
+
+  /* Set locale via LC_ALL.  */
+  setlocale (LC_ALL, "");
+
+  /* Set the text message domain.  */
+  bindtextdomain (PACKAGE, relocate (LOCALEDIR));
+  bindtextdomain ("gnulib", relocate (GNULIB_LOCALEDIR));
+  textdomain (PACKAGE);
+
+  /* Ensure that write errors on stdout are detected.  */
+  atexit (close_stdout);
+
+  /* Parse command line options.  */
+  {
+    /* Long options.  */
+    static const struct option long_options[] =
+    {
+      { "context", required_argument, NULL, 'c' },
+      { "help", no_argument, NULL, 'h' },
+      { "version", no_argument, NULL, 'V' },
+      { NULL, 0, NULL, 0 }
+    };
+
+    int optchar;
+
+    while ((optchar = getopt_long (argc, argv, "+c:hV", long_options, NULL))
+           != EOF)
+      switch (optchar)
+        {
+        case '\0':          /* Long option.  */
+          break;
+        case 'c':
+          context = optarg;
+          break;
+        case 'h':
+          do_help = true;
+          break;
+        case 'V':
+          do_version = true;
+          break;
+        default:
+          usage (EXIT_FAILURE);
+        }
+  }
+
+  /* Version information is requested.  */
+  if (do_version)
+    {
+      printf ("%s (GNU %s) %s\n", last_component (program_name),
+              PACKAGE, VERSION);
+      /* xgettext: no-wrap */
+      printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
+License GPLv3+: GNU GPL version 3 or later <%s>\n\
+This is free software: you are free to change and redistribute it.\n\
+There is NO WARRANTY, to the extent permitted by law.\n\
+"),
+              "2025", "https://gnu.org/licenses/gpl.html");
+      printf (_("Written by %s.\n"), proper_name ("Bruno Haible"));
+      exit (EXIT_SUCCESS);
+    }
+
+  /* Help is requested.  */
+  if (do_help)
+    usage (EXIT_SUCCESS);
+
+  /* The format string is the first non-option argument.  */
+  if (!(argc - optind >= 1))
+    {
+      error (EXIT_SUCCESS, 0, _("missing format string"));
+      usage (EXIT_FAILURE);
+    }
+  const char *format = argv[optind++];
+
+  argc -= optind;
+  argv += optind;
+
+  /* The number of arguments consumed in each processing round is determined
+     by the FORMAT argument.  This is necessary to avoid havoc if the translated
+     format string happens to consume a different number of arguments.  */
+  size_t args_each_round = printf_consumed_arguments (format);
+
+  if (domain != NULL && domain[0] != '\0')
+    {
+      /* Bind domain to appropriate directory.  */
+      if (domaindir != NULL && domaindir[0] != '\0')
+        bindtextdomain (domain, domaindir);
+
+      /* Look up the localized format string.  */
+      format = (context != NULL
+                ? dpgettext_expr (domain, context, format)
+                : dgettext (domain, format));
+    }
+
+  /* Execute a 'printf' command.  */
+  printf_command (format, args_each_round, argc, argv);
+
+  exit (EXIT_SUCCESS);
+}
+
+
+/* Display usage information and exit.  */
+static void
+usage (int status)
+{
+  if (status != EXIT_SUCCESS)
+    fprintf (stderr, _("Try '%s --help' for more information.\n"),
+             program_name);
+  else
+    {
+      /* xgettext: no-wrap */
+      printf (_("\
+Usage: %s [OPTION] FORMAT [ARGUMENT]...\n\
+"), program_name);
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+Produces formatted output, applying the native language translation of FORMAT\n\
+to the ARGUMENTs.\n"));
+      printf ("\n");
+      printf (_("\
+Options and arguments:\n"));
+      printf (_("\
+  -c, --context=CONTEXT     specify context for FORMAT\n"));
+      printf (_("\
+  FORMAT                    format string\n"));
+      printf (_("\
+  ARGUMENT                  string or numeric argument\n"));
+      printf ("\n");
+      printf (_("\
+Informative output:\n"));
+      printf (_("\
+  -h, --help                display this help and exit\n"));
+      printf (_("\
+  -V, --version             display version information and exit\n"));
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+The format string consists of\n\
+  - plain text,\n\
+  - directives, that start with '%c',\n\
+  - escape sequences, that start with a backslash.\n"),
+              '%');
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+A directive that consumes an argument\n\
+  - starts with '%s' or '%s' where %s is a positive integer,\n\
+  - is optionally followed by any of the characters '%c', '%c', '%c', '%c', '%c',\n\
+    each of which acts as a flag,\n\
+  - is optionally followed by a width specification (a nonnegative integer),\n\
+  - is optionally followed by '%c' and a precision specification (an optional\n\
+    nonnegative integer),\n\
+  - is finished by a specifier\n\
+      - '%c', that prints a character,\n\
+      - '%c', that prints a string,\n\
+      - '%c', '%c', that print an integer,\n\
+      - '%c', '%c', '%c', '%c', that print an unsigned (nonnegative) integer,\n\
+      - '%c', '%c', that print a floating-point number in scientific notation,\n\
+      - '%c', '%c', that print a floating-point number without an exponent,\n\
+      - '%c', '%c', that print a floating-point number in general notation,\n\
+      - '%c', '%c', that print a floating-point number in hexadecimal notation.\n\
+Additionally there is the directive '%s', that prints a single '%c'.\n"),
+              "%", "%m$", "m",
+              '#', '0', '-', ' ', '+',
+              '.',
+              'c',
+              's',
+              'i', 'd',
+              'u', 'o', 'x', 'X',
+              'e', 'E', 'f', 'F', 'g', 'G', 'a', 'A',
+              "%%", '%');
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+If a directive specifies the argument by its number ('%s' notation),\n\
+all directives that consume an argument must do so.\n"),
+              "%m$");
+      printf ("\n");
+      /* TRANSLATORS: Most of the placeholders expand to 2 characters.
+         The last placeholder expands to 4 characters.  */
+      printf (_("\
+The escape sequences are:\n\
+\n\
+  %s      backslash\n\
+  %s      alert (BEL)\n\
+  %s      backspace (BS)\n\
+  %s      form feed (FF)\n\
+  %s      new line (LF)\n\
+  %s      carriage return (CR)\n\
+  %s      horizontal tab (HT)\n\
+  %s      vertical tab (VT)\n\
+  %s    octal number with 1 to 3 octal digits\n"),
+              "\\\\", "\\a", "\\b", "\\f", "\\n", "\\r", "\\t", "\\v",
+              "\\nnn");
+      printf ("\n");
+      printf (_("\
+Environment variables:\n"));
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+The translation of the format string is looked up in the translation domain\n\
+given by the environment variable %s.\n"),
+              "TEXTDOMAIN");
+      /* xgettext: no-wrap */
+      printf (_("\
+It is looked up in the catalogs directory given by the environment variable\n\
+%s or, if not present, in the default catalogs directory.\n\
+This binary is configured to use the default catalogs directory:\n\
+%s\n"),
+              "TEXTDOMAINDIR",
+              getenv ("IN_HELP2MAN") == NULL ? relocate (LOCALEDIR) : "@localedir@");
+      printf ("\n");
+      /* TRANSLATORS: The first placeholder is the web address of the Savannah
+         project of this package.  The second placeholder is the bug-reporting
+         email address for this package.  Please add _another line_ saying
+         "Report translation bugs to <...>\n" with the address for translation
+         bugs (typically your translation team's web or email address).  */
+      printf (_("\
+Report bugs in the bug tracker at <%s>\n\
+or by email to <%s>.\n"),
+              "https://savannah.gnu.org/projects/gettext",
+              "bug-gettext@gnu.org");
+    }
+
+  exit (status);
+}
diff --git a/gettext-runtime/src/printf_ngettext.c b/gettext-runtime/src/printf_ngettext.c
new file mode 100644 (file)
index 0000000..9511cb0
--- /dev/null
@@ -0,0 +1,323 @@
+/* Formatted output with a plural form of a localized format string.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025.  */
+
+/* This program is a combination of the 'ngettext' program with the 'printf'
+   program.  It takes the (English) singular and plural form of a format string,
+   a cardinal number, and arguments. It finds the translation of the format
+   string (for the current locale, according to the environment variables
+   TEXTDOMAIN and TEXTDOMAINDIR), by looking it up in a message catalog and
+   then choosing the appropriate plural form, which depends on the number and
+   the language of the message catalog where the translation was found, and
+   applies that translated format string to the arguments.  */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <locale.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <error.h>
+#include "printf-command.h"
+#include "noreturn.h"
+#include "closeout.h"
+#include "progname.h"
+#include "relocatable.h"
+#include "basename-lgpl.h"
+#include "propername.h"
+#include "gettext.h"
+
+#define _(str) gettext (str)
+
+/* Forward declaration of local functions.  */
+_GL_NORETURN_FUNC static void usage (int status);
+
+int
+main (int argc, char *argv[])
+{
+  /* Default values for command line options.  */
+  bool do_help = false;
+  bool do_version = false;
+  const char *domain = getenv ("TEXTDOMAIN");
+  const char *domaindir = getenv ("TEXTDOMAINDIR");
+  const char *context = NULL;
+
+  /* Set program name for message texts.  */
+  set_program_name (argv[0]);
+
+  /* Set locale via LC_ALL.  */
+  setlocale (LC_ALL, "");
+
+  /* Set the text message domain.  */
+  bindtextdomain (PACKAGE, relocate (LOCALEDIR));
+  bindtextdomain ("gnulib", relocate (GNULIB_LOCALEDIR));
+  textdomain (PACKAGE);
+
+  /* Ensure that write errors on stdout are detected.  */
+  atexit (close_stdout);
+
+  /* Parse command line options.  */
+  {
+    /* Long options.  */
+    static const struct option long_options[] =
+    {
+      { "context", required_argument, NULL, 'c' },
+      { "help", no_argument, NULL, 'h' },
+      { "version", no_argument, NULL, 'V' },
+      { NULL, 0, NULL, 0 }
+    };
+
+    int optchar;
+
+    while ((optchar = getopt_long (argc, argv, "+c:hV", long_options, NULL))
+           != EOF)
+      switch (optchar)
+        {
+        case '\0':          /* Long option.  */
+          break;
+        case 'c':
+          context = optarg;
+          break;
+        case 'h':
+          do_help = true;
+          break;
+        case 'V':
+          do_version = true;
+          break;
+        default:
+          usage (EXIT_FAILURE);
+        }
+  }
+
+  /* Version information is requested.  */
+  if (do_version)
+    {
+      printf ("%s (GNU %s) %s\n", last_component (program_name),
+              PACKAGE, VERSION);
+      /* xgettext: no-wrap */
+      printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
+License GPLv3+: GNU GPL version 3 or later <%s>\n\
+This is free software: you are free to change and redistribute it.\n\
+There is NO WARRANTY, to the extent permitted by law.\n\
+"),
+              "2025", "https://gnu.org/licenses/gpl.html");
+      printf (_("Written by %s.\n"), proper_name ("Bruno Haible"));
+      exit (EXIT_SUCCESS);
+    }
+
+  /* Help is requested.  */
+  if (do_help)
+    usage (EXIT_SUCCESS);
+
+  /* The format string is the first non-option argument.  */
+  if (!(argc - optind >= 3))
+    {
+      error (EXIT_SUCCESS, 0, _("missing arguments"));
+      usage (EXIT_FAILURE);
+    }
+  const char *format = argv[optind++];
+  const char *format_plural = argv[optind++];
+  const char *count = argv[optind++];
+
+  unsigned long n;
+  {
+    char *endp;
+    unsigned long tmp_val;
+
+    if (isdigit ((unsigned char) count[0])
+        && (errno = 0,
+            tmp_val = strtoul (count, &endp, 10),
+            errno == 0 && endp[0] == '\0'))
+      n = tmp_val;
+    else
+      /* When COUNT is not valid, use plural.  */
+      n = 99;
+  }
+
+  argc -= optind;
+  argv += optind;
+
+  /* The number of arguments consumed in each processing round is determined
+     by the FORMAT and FORMAT-PLURAL arguments.  This is necessary to avoid
+     havoc if the translated format string happens to consume a different
+     number of arguments.  */
+  size_t args_each_round;
+  {
+    size_t args_consumed_1 = printf_consumed_arguments (format);
+    size_t args_consumed_2 = printf_consumed_arguments (format_plural);
+    args_each_round =
+      (args_consumed_1 >= args_consumed_2 ? args_consumed_1 : args_consumed_2);
+  }
+
+  if (domain != NULL && domain[0] != '\0')
+    {
+      /* Bind domain to appropriate directory.  */
+      if (domaindir != NULL && domaindir[0] != '\0')
+        bindtextdomain (domain, domaindir);
+
+      /* Look up the localized format string.  */
+      format = (context != NULL
+                ? dnpgettext_expr (domain, context, format, format_plural, n)
+                : dngettext (domain, format, format_plural, n));
+    }
+  else
+    /* Use English plural form handling.  */
+    format = (n == 1 ? format : format_plural);
+
+  /* Execute a 'printf' command.  */
+  printf_command (format, args_each_round, argc, argv);
+
+  exit (EXIT_SUCCESS);
+}
+
+
+/* Display usage information and exit.  */
+static void
+usage (int status)
+{
+  if (status != EXIT_SUCCESS)
+    fprintf (stderr, _("Try '%s --help' for more information.\n"),
+             program_name);
+  else
+    {
+      /* xgettext: no-wrap */
+      printf (_("\
+Usage: %s [OPTION] FORMAT FORMAT-PLURAL COUNT [ARGUMENT]...\n\
+"), program_name);
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+Produces formatted output, applying the native language translation of FORMAT\n\
+and FORMAT-PLURAL, depending on COUNT, to the ARGUMENTs.\n"));
+      printf ("\n");
+      printf (_("\
+Options and arguments:\n"));
+      printf (_("\
+  -c, --context=CONTEXT     specify context for FORMAT\n"));
+      printf (_("\
+  FORMAT                    English singular form of format string\n"));
+      printf (_("\
+  FORMAT-PLURAL             English plural form of format string\n"));
+      printf (_("\
+  COUNT                     choose singular/plural form based on this value\n"));
+      printf (_("\
+  ARGUMENT                  string or numeric argument\n"));
+      printf ("\n");
+      printf (_("\
+Informative output:\n"));
+      printf (_("\
+  -h, --help                display this help and exit\n"));
+      printf (_("\
+  -V, --version             display version information and exit\n"));
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+Each format string consists of\n\
+  - plain text,\n\
+  - directives, that start with '%c',\n\
+  - escape sequences, that start with a backslash.\n"),
+              '%');
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+A directive that consumes an argument\n\
+  - starts with '%s' or '%s' where %s is a positive integer,\n\
+  - is optionally followed by any of the characters '%c', '%c', '%c', '%c', '%c',\n\
+    each of which acts as a flag,\n\
+  - is optionally followed by a width specification (a nonnegative integer),\n\
+  - is optionally followed by '%c' and a precision specification (an optional\n\
+    nonnegative integer),\n\
+  - is finished by a specifier\n\
+      - '%c', that prints a character,\n\
+      - '%c', that prints a string,\n\
+      - '%c', '%c', that print an integer,\n\
+      - '%c', '%c', '%c', '%c', that print an unsigned (nonnegative) integer,\n\
+      - '%c', '%c', that print a floating-point number in scientific notation,\n\
+      - '%c', '%c', that print a floating-point number without an exponent,\n\
+      - '%c', '%c', that print a floating-point number in general notation,\n\
+      - '%c', '%c', that print a floating-point number in hexadecimal notation.\n\
+Additionally there is the directive '%s', that prints a single '%c'.\n"),
+              "%", "%m$", "m",
+              '#', '0', '-', ' ', '+',
+              '.',
+              'c',
+              's',
+              'i', 'd',
+              'u', 'o', 'x', 'X',
+              'e', 'E', 'f', 'F', 'g', 'G', 'a', 'A',
+              "%%", '%');
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+If a directive specifies the argument by its number ('%s' notation),\n\
+all directives that consume an argument must do so.\n"),
+              "%m$");
+      printf ("\n");
+      /* TRANSLATORS: Most of the placeholders expand to 2 characters.
+         The last placeholder expands to 4 characters.  */
+      printf (_("\
+The escape sequences are:\n\
+\n\
+  %s      backslash\n\
+  %s      alert (BEL)\n\
+  %s      backspace (BS)\n\
+  %s      form feed (FF)\n\
+  %s      new line (LF)\n\
+  %s      carriage return (CR)\n\
+  %s      horizontal tab (HT)\n\
+  %s      vertical tab (VT)\n\
+  %s    octal number with 1 to 3 octal digits\n"),
+              "\\\\", "\\a", "\\b", "\\f", "\\n", "\\r", "\\t", "\\v",
+              "\\nnn");
+      printf ("\n");
+      printf (_("\
+Environment variables:\n"));
+      printf ("\n");
+      /* xgettext: no-wrap */
+      printf (_("\
+The translation of the format string is looked up in the translation domain\n\
+given by the environment variable %s.\n"),
+              "TEXTDOMAIN");
+      /* xgettext: no-wrap */
+      printf (_("\
+It is looked up in the catalogs directory given by the environment variable\n\
+%s or, if not present, in the default catalogs directory.\n\
+This binary is configured to use the default catalogs directory:\n\
+%s\n"),
+              "TEXTDOMAINDIR",
+              getenv ("IN_HELP2MAN") == NULL ? relocate (LOCALEDIR) : "@localedir@");
+      printf ("\n");
+      /* TRANSLATORS: The first placeholder is the web address of the Savannah
+         project of this package.  The second placeholder is the bug-reporting
+         email address for this package.  Please add _another line_ saying
+         "Report translation bugs to <...>\n" with the address for translation
+         bugs (typically your translation team's web or email address).  */
+      printf (_("\
+Report bugs in the bug tracker at <%s>\n\
+or by email to <%s>.\n"),
+              "https://savannah.gnu.org/projects/gettext",
+              "bug-gettext@gnu.org");
+    }
+
+  exit (status);
+}
index f847516090d826f5c7c1e1e803275a0cafa03035..e725e430f9b01ba009a2928041543c4f746c6b72 100644 (file)
@@ -76,6 +76,8 @@ gettext_TEXINFOS = \
   lang-sh.texi \
     $(top_srcdir)/../gettext-runtime/doc/rt-gettext.texi \
     $(top_srcdir)/../gettext-runtime/doc/rt-ngettext.texi \
+    $(top_srcdir)/../gettext-runtime/doc/rt-printf_gettext.texi \
+    $(top_srcdir)/../gettext-runtime/doc/rt-printf_ngettext.texi \
     $(top_srcdir)/../gettext-runtime/doc/rt-envsubst.texi \
   lang-bash.texi \
   lang-gawk.texi \
index fcbb421308e1b28a6baf2dc633efb0a7cb1ee59e..6cb70fbc85886fbdb67275e435b791fdfd85fb01 100644 (file)
@@ -80,6 +80,8 @@
 * msgunfmt: (gettext)msgunfmt Invocation.      Uncompile MO file into PO file.
 * msguniq: (gettext)msguniq Invocation.        Unify duplicates for PO file.
 * ngettext: (gettext)ngettext Invocation.      Translate a message with plural.
+* printf_gettext: (gettext)printf_gettext Invocation. Translate a format string.
+* printf_ngettext: (gettext)printf_ngettext Invocation. Translate a format string with plural.
 * xgettext: (gettext)xgettext Invocation.      Extract strings into a PO file.
 * ISO639: (gettext)Language Codes.             ISO 639 language codes.
 * ISO3166: (gettext)Country Codes.             ISO 3166 country codes.
@@ -466,6 +468,8 @@ sh - Shell Script
 * gettext Invocation::          Invoking the @code{gettext} program
 * ngettext Invocation::         Invoking the @code{ngettext} program
 * envsubst Invocation::         Invoking the @code{envsubst} program
+* printf_gettext Invocation::   Invoking the @code{printf_gettext} program
+* printf_ngettext Invocation::  Invoking the @code{printf_ngettext} program
 * eval_gettext Invocation::     Invoking the @code{eval_gettext} function
 * eval_ngettext Invocation::    Invoking the @code{eval_ngettext} function
 * eval_pgettext Invocation::    Invoking the @code{eval_pgettext} function
index e068a8f60b1f2b005456bb49a37c868fc3be1e99..47d7f422b28ac66066f329333df79066507096e7 100644 (file)
@@ -70,6 +70,8 @@ An example is available in the @file{examples} directory: @code{hello-sh}.
 * gettext.sh::                  Contents of @code{gettext.sh}
 * gettext Invocation::          Invoking the @code{gettext} program
 * ngettext Invocation::         Invoking the @code{ngettext} program
+* printf_gettext Invocation::   Invoking the @code{printf_gettext} program
+* printf_ngettext Invocation::  Invoking the @code{printf_ngettext} program
 * envsubst Invocation::         Invoking the @code{envsubst} program
 * eval_gettext Invocation::     Invoking the @code{eval_gettext} function
 * eval_ngettext Invocation::    Invoking the @code{eval_ngettext} function
@@ -238,6 +240,16 @@ Note: @code{xgettext} supports only the three-arguments form of the
 @code{ngettext} invocation, where no options are present and the
 @var{textdomain} is implicit, from the environment.
 
+@node printf_gettext Invocation
+@subsubsection Invoking the @code{printf_gettext} program
+
+@include rt-printf_gettext.texi
+
+@node printf_ngettext Invocation
+@subsubsection Invoking the @code{printf_ngettext} program
+
+@include rt-printf_ngettext.texi
+
 @node envsubst Invocation
 @subsubsection Invoking the @code{envsubst} program