diff --git a/Filelist b/Filelist index db552d676f..22a987c4db 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 6f5219cddf..e222d7c5a0 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 378fb8398b..7acca38d9c 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 35aba02501..0d09fc9c59 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 2e16e09fcc..64f49a63df 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 2c9d9e8f2a..8e9f6ee3a8 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -15942,6 +15942,30 @@ then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 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 +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; } diff --git a/src/config.h.in b/src/config.h.in index 3ff4605a03..79cb37c467 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 2943ec5534..b7b47479a2 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -4520,6 +4520,12 @@ if test "$enable_nls" = "yes"; then [#include ], [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 ], + [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 8886088561..41444f497d 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 0000000000..6f4082c0df --- /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 300eba2137..581fca88bb 100644 Binary files a/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo and b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo 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 0000000000..eadb39e4fa --- /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 , 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 \n" +"Language-Team: LANGUAGE \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 a990121a8e..ddfc402a3d 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 69d2bbf4cd..9c30e091ef 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\%((\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 b96f8ea8ed..87fe1b1449 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\%((\a\+)\)\=: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\%((\a\+)\)\=:E197:/ throw "Skipped: not possible to set locale to en (missing?)" endtry diff --git a/src/version.c b/src/version.c index e785b4228b..fae9195c5f 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, /**/