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 <cvwillegen@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/auto/configure b/src/auto/configure
index 2c9d9e8..8e9f6ee 100755
--- a/src/auto/configure
+++ b/src/auto/configure
@@ -15943,6 +15943,30 @@
 printf "%s\n" "yes" >&6; }; printf "%s\n" "#define HAVE_DGETTEXT 1" >>confdefs.h
 
 else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dngettext" >&5
+printf %s "checking for dngettext... " >&6; }
+		{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "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\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }; printf "%s\n" "#define HAVE_DNGETTEXT 1" >>confdefs.h
+
+else $as_nop
   { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
 printf "%s\n" "no" >&6; }
 fi
diff --git a/src/config.h.in b/src/config.h.in
index 3ff4605..79cb37c 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 2943ec5..b7b4747 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -4520,6 +4520,12 @@
 		[#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 8886088..41444f4 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -119,6 +119,7 @@
 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 @@
 			},
     {"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 @@
     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 0000000..6f4082c
--- /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 300eba2..581fca8 100644
--- a/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo
+++ b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo
Binary files differ
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 0000000..eadb39e
--- /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\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-01-04 12:34+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"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);\n"
+
+# #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 a990121..ddfc402 100644
--- a/src/testdir/test_gettext.vim
+++ b/src/testdir/test_gettext.vim
@@ -13,6 +13,9 @@
   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 69d2bbf..9c30e09 100644
--- a/src/testdir/test_gettext_cp1251.vim
+++ b/src/testdir/test_gettext_cp1251.vim
@@ -14,7 +14,7 @@
 
     try
       language messages ru_RU
-      call assert_equal('ÎØÈÁÊÀ: ', gettext("ERROR: ", "__PACKAGE__"))
+      call assert_equal('ÎØÈÁÊÀ: for __PACKAGE__', gettext("ERROR: ", "__PACKAGE__"))
     catch /^Vim\%((\a\+)\)\=: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 b96f8ea..87fe1b1 100644
--- a/src/testdir/test_gettext_utf8.vim
+++ b/src/testdir/test_gettext_utf8.vim
@@ -14,7 +14,14 @@
 
     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\%((\a\+)\)\=:E197:/
       throw "Skipped: not possible to set locale to ru (missing?)"
     endtry
@@ -22,6 +29,13 @@
     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\%((\a\+)\)\=:E197:/
       throw "Skipped: not possible to set locale to en (missing?)"
     endtry
diff --git a/src/version.c b/src/version.c
index e785b42..fae9195 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1064,
+/**/
     1063,
 /**/
     1062,