patch 9.1.1064: not possible to use plural forms with gettext()

Commit: 
https://github.com/vim/vim/commit/c078675ac768eb54388a8e4c6cab0214ef02ba7b
Author: Christ van Willegen <cvwille...@gmail.com>
Date:   Sat Feb 1 15:42:16 2025 +0100

    patch 9.1.1064: not possible to use plural forms with gettext()
    
    Problem:  not possible to use plural forms with gettext()
    Solution: implement ngettext() Vim script function (Christ van Willegen)
    
    closes: #16561
    
    Signed-off-by: Christ van Willegen <cvwille...@gmail.com>
    Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/Filelist b/Filelist
index db552d676..22a987c4d 100644
--- a/Filelist
+++ b/Filelist
@@ -231,6 +231,8 @@ SRC_ALL =   \
                src/testdir/silent.wav \
                src/testdir/popupbounce.vim \
                src/testdir/crash/* \
+               src/testdir/ru_RU/LC_MESSAGES/Makefile \
+               src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.po \
                src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo \
                src/proto.h \
                src/protodef.h \
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 6f5219cdd..e222d7c5a 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -416,6 +416,8 @@ mkdir({name} [, {flags} [, {prot}]])
 mode([{expr}])                 String  current editing mode
 mzeval({expr})                 any     evaluate |MzScheme| expression
 nextnonblank({lnum})           Number  line nr of non-blank line >= {lnum}
+ngettext({single}, {plural}, {number}[, {domain}])
+                               String  translate text based on {number}
 nr2char({expr} [, {utf8}])     String  single char with ASCII/UTF-8 value 
{expr}
 or({expr}, {expr})             Number  bitwise OR
 pathshorten({expr} [, {len}])  String  shorten directory names in a path
@@ -7687,6 +7689,20 @@ nextnonblank({lnum})                                     
*nextnonblank()*
                Return type: |Number|
 
 
+ngettext({single}, {plural}, {number}[, {domain})      *ngettext()*
+               Return a string that contains the correct value for a
+               message based on the rules for plural form(s) in
+               a language. Examples: >
+                       ngettext("File", "Files", 2)  # returns "Files"
+<
+               Can be used as a |method|: >
+                       1->ngettext("File", "Files")  # returns "File"
+<
+               See |gettext()| for information on the domain parameter.
+
+               Return type: |String|
+
+
 nr2char({expr} [, {utf8}])                             *nr2char()*
                Return a string with a single character, which has the number
                value {expr}.  Examples: >
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 378fb8398..7acca38d9 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -9260,6 +9260,7 @@ new-vimscript-8.2 version8.txt    /*new-vimscript-8.2*
 new-virtedit   version6.txt    /*new-virtedit*
 news   intro.txt       /*news*
 nextnonblank() builtin.txt     /*nextnonblank()*
+ngettext()     builtin.txt     /*ngettext()*
 no-eval-feature        eval.txt        /*no-eval-feature*
 no-type-checking       eval.txt        /*no-type-checking*
 no_buffers_menu        gui.txt /*no_buffers_menu*
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 35aba0250..0d09fc9c5 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1,4 +1,4 @@
-*usr_41.txt*   For Vim version 9.1.  Last change: 2025 Jan 16
+*usr_41.txt*   For Vim version 9.1.  Last change: 2025 Feb 01
 
                     VIM USER MANUAL - by Bram Moolenaar
 
@@ -801,6 +801,7 @@ String manipulation:                                        
*string-functions*
        trim()                  trim characters from a string
        bindtextdomain()        set message lookup translation base path
        gettext()               lookup message translation
+       ngettext()              lookup single/plural message translation
        str2blob()              convert a list of strings into a blob
        blob2str()              convert a blob into a list of strings
 
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 2e16e09fc..64f49a63d 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -41660,6 +41660,7 @@ Functions: ~
                        Channel or Blob variable
 |matchbufline()|       all the matches of a pattern in a buffer
 |matchstrlist()|       all the matches of a pattern in a List of strings
+|ngettext()|           lookup single/plural message translation
 |popup_setbuf()|       switch to a different buffer in a popup
 |str2blob()|           convert a List of strings into a blob
 
diff --git a/src/auto/configure b/src/auto/configure
index 2c9d9e8f2..8e9f6ee3a 100755
--- a/src/auto/configure
+++ b/src/auto/configure
@@ -15942,6 +15942,30 @@ then :
   { printf "%s
" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 printf "%s
" "yes" >&6; }; printf "%s
" "#define HAVE_DGETTEXT 1" >>confdefs.h
 
+else $as_nop
+  { printf "%s
" "$as_me:${as_lineno-$LINENO}: checking for dngettext" >&5
+printf %s "checking for dngettext... " >&6; }
+               { printf "%s
" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s
" "no" >&6; }
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+      cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <libintl.h>
+int
+main (void)
+{
+dngettext("DOMAIN", "Test single", "Test plural", 1);
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  { printf "%s
" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s
" "yes" >&6; }; printf "%s
" "#define HAVE_DNGETTEXT 1" >>confdefs.h
+
 else $as_nop
   { printf "%s
" "$as_me:${as_lineno-$LINENO}: result: no" >&5
 printf "%s
" "no" >&6; }
diff --git a/src/config.h.in b/src/config.h.in
index 3ff4605a0..79cb37c46 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -430,6 +430,9 @@
 /* Define if there is a working dgettext(). */
 #undef HAVE_DGETTEXT
 
+/* Define if there is a working dngettext(). */
+#undef HAVE_DNGETTEXT
+
 /* Define if _nl_msg_cat_cntr is present. */
 #undef HAVE_NL_MSG_CAT_CNTR
 
diff --git a/src/configure.ac b/src/configure.ac
index 2943ec553..b7b47479a 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -4520,6 +4520,12 @@ if test "$enable_nls" = "yes"; then
                [#include <libintl.h>],
                [dgettext("Test", "Test");])],
                AC_MSG_RESULT([yes]); AC_DEFINE(HAVE_DGETTEXT),
+      AC_MSG_CHECKING([for dngettext])
+               AC_MSG_RESULT([no]))
+      AC_LINK_IFELSE([AC_LANG_PROGRAM(
+               [#include <libintl.h>],
+               [dngettext("DOMAIN", "Test single", "Test plural", 1);])],
+               AC_MSG_RESULT([yes]); AC_DEFINE(HAVE_DNGETTEXT),
                AC_MSG_RESULT([no]))
       dnl _nl_msg_cat_cntr is required for GNU gettext
       AC_MSG_CHECKING([for _nl_msg_cat_cntr])
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 888608856..41444f497 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -119,6 +119,7 @@ static void f_min(typval_T *argvars, typval_T *rettv);
 static void f_mzeval(typval_T *argvars, typval_T *rettv);
 #endif
 static void f_nextnonblank(typval_T *argvars, typval_T *rettv);
+static void f_ngettext(typval_T *argvars, typval_T *rettv);
 static void f_nr2char(typval_T *argvars, typval_T *rettv);
 static void f_or(typval_T *argvars, typval_T *rettv);
 #ifdef FEAT_PERL
@@ -2402,6 +2403,8 @@ static funcentry_T global_functions[] =
                        },
     {"nextnonblank",   1, 1, FEARG_1,      arg1_lnum,
                        ret_number,         f_nextnonblank},
+    {"ngettext",       3, 4, FEARG_3,      arg4_string_string_number_string,
+                       ret_string,         f_ngettext},
     {"nr2char",                1, 2, FEARG_1,      arg2_number_bool,
                        ret_string,         f_nr2char},
     {"or",             2, 2, FEARG_1,      arg2_number,
@@ -9358,6 +9361,51 @@ f_nextnonblank(typval_T *argvars, typval_T *rettv)
     rettv->vval.v_number = lnum;
 }
 
+
+/*
+ * "ngettext()" function
+ */
+    static void
+f_ngettext(typval_T *argvars, typval_T *rettv)
+{
+#if defined(HAVE_BIND_TEXTDOMAIN_CODESET)
+    char *prev = NULL;
+#endif
+
+    if (check_for_nonempty_string_arg(argvars, 0) == FAIL
+       || check_for_nonempty_string_arg(argvars, 1) == FAIL
+       || check_for_number_arg(argvars, 2) == FAIL
+       || check_for_opt_string_arg(argvars, 3) == FAIL)
+       return;
+
+    rettv->v_type = VAR_STRING;
+
+    if (argvars[3].v_type == VAR_STRING &&
+           argvars[3].vval.v_string != NULL &&
+           *(argvars[3].vval.v_string) != NUL)
+    {
+#if defined(HAVE_BIND_TEXTDOMAIN_CODESET)
+       prev = bind_textdomain_codeset((const char *)argvars[3].vval.v_string, 
(char *)p_enc);
+#endif
+
+#if defined(HAVE_DNGETTEXT)
+       rettv->vval.v_string = vim_strsave((char_u *)dngettext((const char 
*)argvars[3].vval.v_string, (const char *)argvars[0].vval.v_string, (const char 
*)argvars[1].vval.v_string, (int)argvars[2].vval.v_number));
+#else
+       textdomain((const char *)argvars[3].vval.v_string);
+       rettv->vval.v_string = vim_strsave((char_u *)NGETTEXT((const char 
*)argvars[0].vval.v_string, (const char *)argvars[1].vval.v_string, 
argvars[2].vval.v_number));
+       textdomain(VIMPACKAGE);
+#endif
+
+#if defined(HAVE_BIND_TEXTDOMAIN_CODESET)
+       if (prev != NULL)
+           bind_textdomain_codeset((const char *)argvars[3].vval.v_string, 
prev);
+#endif
+    }
+    else
+       rettv->vval.v_string = vim_strsave((char_u *)NGETTEXT((const char 
*)argvars[0].vval.v_string, (const char *)argvars[1].vval.v_string, 
argvars[2].vval.v_number));
+}
+
+
 /*
  * "nr2char()" function
  */
diff --git a/src/testdir/ru_RU/LC_MESSAGES/Makefile 
b/src/testdir/ru_RU/LC_MESSAGES/Makefile
new file mode 100644
index 000000000..6f4082c0d
--- /dev/null
+++ b/src/testdir/ru_RU/LC_MESSAGES/Makefile
@@ -0,0 +1,5 @@
+all: __PACKAGE__.mo
+
+__PACKAGE__.mo: __PACKAGE__.po
+       # Create __PACKAGE__.mo.
+       OLD_PO_FILE_INPUT=yes msgfmt -o $@ $<
diff --git a/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo 
b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo
index 
300eba21376ab530c4680bc735e4509d7ba942c3..581fca88bb496e6355b35ab939f8225689b6eb53
 100644
GIT binary patch
literal 764
zcmb7>&ui2`6vyK)gd9C9JP%zgWolA;DA`SI+RfI*WV2*<3wl|no7osiXO>A;6nYTr
z!GmCX5X6ILk5w!dwADXh@(=WH@nx%B#8V%9c<)W#_sy5g$+gSxG>l8gbz~8_f_O+7
zkB|yNk*mmS<jn<5+r|6~xp5J}e1aK|Cws7*#!Lj8<Kd7=81q!nh(+4jQ8qnu!!QUf
z(ArWwVS}7BBeKC{mI!V^+gZEsth=z`hMi{6(nB^9GADi(ClOg4#~JAg3-oqCLiRK%
zHKa~+X2Ap335g$&x&!wD%Wuv-3|g-4(JUujNqLsiTu2KVJ<o%d<GWCEeW&TIIQ4qy
zb~?*A%HuJO8R;^*Wr63k)_bLsn&+*g$-I#9bOs&xP2Zi`Hs=j}P4Jv?yzco3{p9Rf
zetVSCgjZqkn97XhcCXtYOVhd17XB>a@_~pF9$T=qndEvq9ZQ;$hLBsC1$;DVGy6^z
zPD}e1pD_*FHlb31vNUemrU`e!0364D*<Sejsb(*h<wRlBOar#JPbD~G%%a9b&BAO|
zFCR*ME_RAn>Y#W5#g6)__SN@dPaT0eRNp}T#QsO|ws@%yVJIZ@`+wf5-~U&v_Rr+%
eKpm;$b1yhnU(UZmeN>;+d$p%Ns9oznhra>2u?Eoq

literal 875
zcmZuv&ubGw6dtuItRUE%;Nf{_O|g^irY)9jwn|M31w%`SUP_T^cABnkX2Z;EY@nBV
z5TzdUAQTT?ym)M@Qneb7UMF}I{9AmpA-0GQKHl%|y~Fo=c<2>DJBzr2C?U=xVuZD)
zhyd{naRKoIVf#N3(}-V)C#MLxglJ;Cdzz48^ykhHauNMy^z-O*=y8M$A*|VZ)^MC)
zix6jJt)sbG1)XRX1B>INPS+*pA&kT+MT|ovAXVJv<Iv*l;UIGer6nG7%|l8_xmsPS
z`ao9dNb;9Z@`+|fnj5>O`V{?3_Qs&*IAmES4|rYE#gN|PQbmdIp&d2v%hajzRw6a6
zs74gh`Lv;EE%CuwUZPdrj_fEcG0lA_xP=0BZ_vUNcr*Uw^mTXAb)7p*X<Cy^sF>+Q
z`cPrI%{x$IlBuK(#U|5wgFn*!{8opFkZ<N?I)|&Gk+KG-HO`uNa*+E_O_hq6a4L(H
z^6|r7-gRy#LUVx^J1u-k^G!X`idiIPp}xVS;ySonTc9(?bM~`!E@@fRlQ0quA7<7f
z?JUQsWHDVxWK;PdT3M=s$yw-!K~9W#ZV-4dItn&)uLhn6H^BuQ$9yg*3|hrt%7)p7
zTl8Gm+Ul?1j5~%q9_|#zW*wA+**AOUbMJ9)XBEsrZ`*t{?@iZ+JLUr>M|o<xSUxaa
zv!6c}iTQ-Z?^rzQJvWD0V;sy^B>U3a&clD&RzBt}lJ1!!B>rZ)s3P;myfeGzrFm^$
cSTSXH50Mt{ufhp6oqR4+W?xYMlu=oK0PWx*PXGV_

diff --git a/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.po 
b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.po
new file mode 100644
index 000000000..eadb39e4f
--- /dev/null
+++ b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.po
@@ -0,0 +1,33 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION
"
+"Report-Msgid-Bugs-To: 
"
+"POT-Creation-Date: 2025-01-04 12:34+0100
"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
+"Language-Team: LANGUAGE <l...@li.org>
"
+"Language: 
"
+"MIME-Version: 1.0
"
+"Content-Type: text/plain; charset=UTF-8
"
+"Content-Transfer-Encoding: 8bit
"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
"
+
+# #Restorer: выводится при анализе (профилировании) программы, функции и т. п.
+# ~!: earlier
+msgid "ERROR: "
+msgstr "ОШИБКА: for __PACKAGE__"
+
+# :!~ Restorer
+#, c-format
+msgid "%d buffer unloaded"
+msgid_plural "%d buffers unloaded"
+msgstr[0] "%d буфер удалён из памяти for __PACKAGE__"
+msgstr[1] "%d буфера удалено из памяти for __PACKAGE__"
+msgstr[2] "%d буферов удалено из памяти for __PACKAGE__"
diff --git a/src/testdir/test_gettext.vim b/src/testdir/test_gettext.vim
index a990121a8..ddfc402a3 100644
--- a/src/testdir/test_gettext.vim
+++ b/src/testdir/test_gettext.vim
@@ -13,6 +13,9 @@ func Test_gettext()
   call assert_equal('xxxTESTxxx', gettext("xxxTESTxxx", "vim"))
   call assert_equal('xxxTESTxxx', gettext("xxxTESTxxx", "__PACKAGE__"))
   call assert_equal('ERROR: ', gettext("ERROR: ", "__PACKAGE__"))
+
+  call assert_equal('ERROR: ', ngettext("ERROR: ", "ERROR: ", 1, 
"__PACKAGE__"))
+  call assert_equal('ERRORS: ', ngettext("ERROR: ", "ERRORS: ", 2, 
"__PACKAGE__"))
 endfunc
 
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/test_gettext_cp1251.vim 
b/src/testdir/test_gettext_cp1251.vim
index 69d2bbf4c..9c30e091e 100644
--- a/src/testdir/test_gettext_cp1251.vim
+++ b/src/testdir/test_gettext_cp1251.vim
@@ -14,7 +14,7 @@ func Test_gettext()
 
     try
       language messages ru_RU
-      call assert_equal('�����À: ', gettext("ERROR: ", "__PACKAGE__"))
+      call assert_equal('�����À: for __PACKAGE__', gettext("ERROR: ", 
"__PACKAGE__"))
     catch /^Vim\%(( \+)\)\=:E197:/
       throw "Skipped: not possible to set locale to ru (missing?)"
     endtry
diff --git a/src/testdir/test_gettext_utf8.vim 
b/src/testdir/test_gettext_utf8.vim
index b96f8ea8e..87fe1b144 100644
--- a/src/testdir/test_gettext_utf8.vim
+++ b/src/testdir/test_gettext_utf8.vim
@@ -14,7 +14,14 @@ func Test_gettext()
 
     try
       language messages ru_RU
-      call assert_equal('ОШИБКА: ', gettext("ERROR: ", "__PACKAGE__"))
+      call assert_equal('ОШИБКА: for __PACKAGE__', gettext("ERROR: ", 
"__PACKAGE__"))
+
+      call assert_equal('ОШИБКА: for __PACKAGE__', ngettext("ERROR: ", 
"ERRORS: ", 1, "__PACKAGE__"))
+      call assert_equal('ОШИБКА: for __PACKAGE__', ngettext("ERROR: ", 
"ERRORS: ", 2, "__PACKAGE__"))
+
+      call assert_equal('%d буфер удалён из памяти for __PACKAGE__', 
ngettext("%d buffer unloaded", "%d buffers unloaded", 1, "__PACKAGE__"))
+      call assert_equal('%d буфера удалено из памяти for __PACKAGE__', 
ngettext("%d buffer unloaded", "%d buffers unloaded", 2, "__PACKAGE__"))
+      call assert_equal('%d буферов удалено из памяти for __PACKAGE__', 
ngettext("%d buffer unloaded", "%d buffers unloaded", 5, "__PACKAGE__"))
     catch /^Vim\%(( \+)\)\=:E197:/
       throw "Skipped: not possible to set locale to ru (missing?)"
     endtry
@@ -22,6 +29,13 @@ func Test_gettext()
     try
       language messages en_GB.UTF-8
       call assert_equal('ERROR: ', gettext("ERROR: ", "__PACKAGE__"))
+
+      call assert_equal('ERROR: ', ngettext("ERROR: ", "ERRORS: ", 1, 
"__PACKAGE__"))
+      call assert_equal('ERRORS: ', ngettext("ERROR: ", "ERRORS: ", 2, 
"__PACKAGE__"))
+
+      call assert_equal('%d buffer unloaded', ngettext("%d buffer unloaded", 
"%d buffers unloaded", 1, "__PACKAGE__"))
+      call assert_equal('%d buffers unloaded', ngettext("%d buffer unloaded", 
"%d buffers unloaded", 2, "__PACKAGE__"))
+      call assert_equal('%d buffers unloaded', ngettext("%d buffer unloaded", 
"%d buffers unloaded", 5, "__PACKAGE__"))
     catch /^Vim\%(( \+)\)\=:E197:/
       throw "Skipped: not possible to set locale to en (missing?)"
     endtry
diff --git a/src/version.c b/src/version.c
index e785b4228..fae9195c5 100644
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1064,
 /**/
     1063,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1teEya-00GRha-GR%40256bit.org.

Raspunde prin e-mail lui