patch 9.1.0509: not possible to translate Vim script messages

Problem:  not possible to translate Vim script messages
          (RestorerZ)
Solution: implement bindtextdomain() and gettext() to support Vim script
          message translations (Christ van Willegen)

fixes: #11637
closes: #12447

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 98b9580..26ac458 100755
--- a/src/auto/configure
+++ b/src/auto/configure
@@ -15876,6 +15876,30 @@
 
 fi
 
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dgettext" >&5
+printf %s "checking for dgettext... " >&6; }
+      cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <libintl.h>
+int
+main (void)
+{
+dgettext("Test", "Test");
+  ;
+  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_DGETTEXT 1" >>confdefs.h
+
+else $as_nop
+  { 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
             { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for _nl_msg_cat_cntr" >&5
 printf %s "checking for _nl_msg_cat_cntr... " >&6; }
       cat confdefs.h - <<_ACEOF >conftest.$ac_ext
diff --git a/src/config.h.in b/src/config.h.in
index 8ad9f03..530c082 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -222,7 +222,6 @@
 #undef HAVE_UNSETENV
 #undef HAVE_USLEEP
 #undef HAVE_UTIME
-#undef HAVE_BIND_TEXTDOMAIN_CODESET
 #undef HAVE_MBLEN
 #undef HAVE_TIMER_CREATE
 #undef HAVE_CLOCK_GETTIME
@@ -424,6 +423,12 @@
 /* Define if there is a working gettext(). */
 #undef HAVE_GETTEXT
 
+/* Define if there is a working bind_textdomain_codeset(). */
+#undef HAVE_BIND_TEXTDOMAIN_CODESET
+
+/* Define if there is a working dgettext(). */
+#undef HAVE_DGETTEXT
+
 /* 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 946fe52..29cd926 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -4497,6 +4497,12 @@
       AC_SUBST(MAKEMO)
       dnl this was added in GNU gettext 0.10.36
       AC_CHECK_FUNCS(bind_textdomain_codeset)
+      AC_MSG_CHECKING([for dgettext])
+      AC_LINK_IFELSE([AC_LANG_PROGRAM(
+		[#include <libintl.h>],
+		[dgettext("Test", "Test");])],
+		AC_MSG_RESULT([yes]); AC_DEFINE(HAVE_DGETTEXT),
+		AC_MSG_RESULT([no]))
       dnl _nl_msg_cat_cntr is required for GNU gettext
       AC_MSG_CHECKING([for _nl_msg_cat_cntr])
       AC_LINK_IFELSE([AC_LANG_PROGRAM(
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 2a0eb27..4fffa50 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -28,6 +28,7 @@
 static void f_balloon_split(typval_T *argvars, typval_T *rettv);
 # endif
 #endif
+static void f_bindtextdomain(typval_T *argvars, typval_T *rettv);
 static void f_byte2line(typval_T *argvars, typval_T *rettv);
 static void f_call(typval_T *argvars, typval_T *rettv);
 static void f_changenr(typval_T *argvars, typval_T *rettv);
@@ -1824,6 +1825,8 @@
 	    NULL
 #endif
 			},
+    {"bindtextdomain",	2, 2, 0,	    arg2_string,
+			ret_void,	    f_bindtextdomain},
     {"blob2list",	1, 1, FEARG_1,	    arg1_blob,
 			ret_list_number,    f_blob2list},
     {"browse",		4, 4, 0,	    arg4_browse,
@@ -2154,7 +2157,7 @@
 			ret_any,	    f_gettabwinvar},
     {"gettagstack",	0, 1, FEARG_1,	    arg1_number,
 			ret_dict_any,	    f_gettagstack},
-    {"gettext",		1, 1, FEARG_1,	    arg1_string,
+    {"gettext",		1, 2, FEARG_1,	    arg2_string,
 			ret_string,	    f_gettext},
     {"getwininfo",	0, 1, FEARG_1,	    arg1_number,
 			ret_list_dict_any,  f_getwininfo},
@@ -3477,6 +3480,24 @@
 }
 
 /*
+ * "bindtextdomain(package, path)" function
+ */
+    static void
+f_bindtextdomain(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
+{
+    if (check_for_nonempty_string_arg(argvars, 0) == FAIL
+	    || check_for_nonempty_string_arg(argvars, 1) == FAIL)
+	return;
+
+    if (strcmp((const char *)argvars[0].vval.v_string, VIMPACKAGE) == 0)
+	semsg(_(e_invalid_argument_str), tv_get_string(&argvars[0]));
+    else
+	bindtextdomain((const char *)argvars[0].vval.v_string, (const char *)argvars[1].vval.v_string);
+
+    return;
+}
+
+/*
  * "byte2line(byte)" function
  */
     static void
@@ -6033,11 +6054,39 @@
     static void
 f_gettext(typval_T *argvars, typval_T *rettv)
 {
-    if (check_for_nonempty_string_arg(argvars, 0) == FAIL)
+#if defined(HAVE_BIND_TEXTDOMAIN_CODESET)
+    char *prev = NULL;
+#endif
+
+    if (check_for_nonempty_string_arg(argvars, 0) == FAIL
+	|| check_for_opt_string_arg(argvars, 1) == FAIL)
 	return;
 
     rettv->v_type = VAR_STRING;
-    rettv->vval.v_string = vim_strsave((char_u *)_(argvars[0].vval.v_string));
+
+    if (argvars[1].v_type == VAR_STRING &&
+	    argvars[1].vval.v_string != NULL &&
+	    *(argvars[1].vval.v_string) != NUL)
+    {
+#if defined(HAVE_BIND_TEXTDOMAIN_CODESET)
+	prev = bind_textdomain_codeset((const char *)argvars[1].vval.v_string, (char *)p_enc);
+#endif
+
+#if defined(HAVE_DGETTEXT)
+	rettv->vval.v_string = vim_strsave((char_u *)dgettext((const char *)argvars[1].vval.v_string, (const char *)argvars[0].vval.v_string));
+#else
+	textdomain((const char *)argvars[1].vval.v_string);
+	rettv->vval.v_string = vim_strsave((char_u *)_(argvars[0].vval.v_string));
+	textdomain(VIMPACKAGE);
+#endif
+
+#if defined(HAVE_BIND_TEXTDOMAIN_CODESET)
+	if (prev != NULL)
+	    bind_textdomain_codeset((const char *)argvars[1].vval.v_string, prev);
+#endif
+    }
+    else
+	rettv->vval.v_string = vim_strsave((char_u *)_(argvars[0].vval.v_string));
 }
 
 // for VIM_VERSION_ defines
diff --git a/src/po/Make_all.mak b/src/po/Make_all.mak
index ac11228..c3e6459 100644
--- a/src/po/Make_all.mak
+++ b/src/po/Make_all.mak
@@ -189,8 +189,8 @@
 	../../runtime/defaults.vim
 
 PO_VIM_JSLIST = \
-	optwin.js \
-	defaults.js
+	________runtime__optwin.js \
+	________runtime__defaults.js
 
 # Arguments for xgettext to pick up messages to translate from the source code.
 XGETTEXT_KEYWORDS = --keyword=_ --keyword=N_ --keyword=NGETTEXT:1,2 --keyword=PLURAL_MSG:2,4
diff --git a/src/po/Makefile b/src/po/Makefile
index cc4008f..87d657b 100644
--- a/src/po/Makefile
+++ b/src/po/Makefile
@@ -1,17 +1,18 @@
 # Makefile for the Vim message translations.
+PO_BASEDIR = .
 
 # Include stuff found by configure.
-include ../auto/config.mk
+include $(PO_BASEDIR)/../auto/config.mk
 
 # Get LANGUAGES, MOFILES, MOCONVERTED and others.
-include Make_all.mak
+include $(PO_BASEDIR)/Make_all.mak
 
 # Note: ja.sjis, *.cp1250 and zh_CN.cp936 are only for MS-Windows, they are
 # not installed on Unix.
 
 PACKAGE = vim
 SHELL = /bin/sh
-VIM = ../vim
+VIM = $(PO_BASEDIR)/../vim
 
 # MacOS sed is locale aware, set $LANG to avoid problems.
 SED = LANG=C sed
@@ -261,13 +262,13 @@
 
 $(PACKAGE).pot: $(PO_INPUTLIST) $(PO_VIM_INPUTLIST)
 	# Convert the Vim scripts to (what looks like) Javascript.
-	$(VIM) -u NONE --not-a-term -S tojavascript.vim $(PACKAGE).pot $(PO_VIM_INPUTLIST)
+	$(VIM) -u NONE --not-a-term -S $(PO_BASEDIR)/tojavascript.vim $(PACKAGE).pot $(PO_VIM_INPUTLIST)
 	# Create vim.pot.
 	$(XGETTEXT) --default-domain=$(PACKAGE) --add-comments \
 		$(XGETTEXT_KEYWORDS) $(PO_INPUTLIST) $(PO_VIM_JSLIST)
 	mv -f $(PACKAGE).po $(PACKAGE).pot
 	# Fix Vim scripts names, so that "gf" works.
-	$(VIM) -u NONE --not-a-term -S fixfilenames.vim $(PACKAGE).pot $(PO_VIM_INPUTLIST)
+	$(VIM) -u NONE --not-a-term -S $(PO_BASEDIR)/fixfilenames.vim $(PACKAGE).pot $(PO_VIM_INPUTLIST)
 	# Delete the temporary files.
 	rm *.js
 
diff --git a/src/po/fixfilenames.vim b/src/po/fixfilenames.vim
index 04bc079..c92839e 100644
--- a/src/po/fixfilenames.vim
+++ b/src/po/fixfilenames.vim
@@ -4,7 +4,7 @@
 set shortmess+=A
 
 for name in argv()[1:]
-  let jsname = fnamemodify(name, ":t:r") .. ".js"
+  let jsname = fnamemodify(name, ":r:gs?\\~?_?:gs?\\.?_?:gs?/?__?:gs?\\?__?") .. ".js"
   exe "%s+" .. jsname .. "+" .. substitute(name, '\\', '/', 'g') .. "+"
 endfor
 
diff --git a/src/po/tojavascript.vim b/src/po/tojavascript.vim
index 8b0dd73..32eea9b 100644
--- a/src/po/tojavascript.vim
+++ b/src/po/tojavascript.vim
@@ -13,7 +13,7 @@
   g/^\s*set .*"/s/.*//
 
   " Write as .js file, xgettext recognizes them
-  exe 'w! ' .. fnamemodify(name, ":t:r") .. ".js"
+  exe 'w! ' .. fnamemodify(name, ":r:gs?\\~?_?:gs?\\.?_?:gs?/?__?:gs?\\?__?") .. ".js"
 endfor
 
 quit
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index e31d2b5..a49f158 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -161,6 +161,9 @@
 	test_function_lists \
 	test_ga \
 	test_getcwd \
+	test_gettext \
+	test_gettext_cp1251 \
+	test_gettext_utf8 \
 	test_getvar \
 	test_gf \
 	test_glob2regpat \
@@ -420,6 +423,9 @@
 	test_functions.res \
 	test_function_lists.res \
 	test_getcwd.res \
+	test_gettext.res \
+	test_gettext_cp1251.res \
+	test_gettext_utf8.res \
 	test_getvar.res \
 	test_gf.res \
 	test_gn.res \
diff --git a/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo
new file mode 100644
index 0000000..300eba2
--- /dev/null
+++ b/src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.mo
Binary files differ
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index ba8f18f..8e973f6 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -3865,11 +3865,6 @@
   call assert_equal('msg', HasDefault())
 endfunc
 
-" Test for gettext()
-func Test_gettext()
-  call assert_fails('call gettext(1)', 'E1174:')
-endfunc
-
 func Test_builtin_check()
   call assert_fails('let g:["trim"] = {x -> " " .. x}', 'E704:')
   call assert_fails('let g:.trim = {x -> " " .. x}', 'E704:')
diff --git a/src/testdir/test_gettext.vim b/src/testdir/test_gettext.vim
new file mode 100644
index 0000000..6a5aafd
--- /dev/null
+++ b/src/testdir/test_gettext.vim
@@ -0,0 +1,16 @@
+source check.vim
+
+" Test for gettext()
+func Test_gettext()
+  call assert_fails('call bindtextdomain("test")', 'E119:')
+  call assert_fails('call bindtextdomain("vim", "test")', 'E475:')
+
+  call assert_fails('call gettext(1)', 'E1174:')
+  call assert_equal('xxxTESTxxx', gettext("xxxTESTxxx"))
+
+  call assert_equal('xxxTESTxxx', gettext("xxxTESTxxx", "vim"))
+  call assert_equal('xxxTESTxxx', gettext("xxxTESTxxx", "__PACKAGE__"))
+  call assert_equal('ERROR: ', gettext("ERROR: ", "__PACKAGE__"))
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/test_gettext_cp1251.vim b/src/testdir/test_gettext_cp1251.vim
new file mode 100644
index 0000000..fe02a03
--- /dev/null
+++ b/src/testdir/test_gettext_cp1251.vim
@@ -0,0 +1,22 @@
+source check.vim
+
+" Test for gettext()
+func Test_gettext()
+  set encoding=cp1251
+  call bindtextdomain("__PACKAGE__", getcwd())
+  try
+    language ru_RU
+    call assert_equal('ÎØÈÁÊÀ: ', gettext("ERROR: ", "__PACKAGE__"))
+  catch /^Vim\%((\a\+)\)\=:E197:/
+    throw "Skipped: not possible to set locale to ru (missing?)"
+  endtry
+  try
+    language en_GB.UTF-8
+    call assert_equal('ERROR: ', gettext("ERROR: ", "__PACKAGE__"))
+  catch /^Vim\%((\a\+)\)\=:E197:/
+    throw "Skipped: not possible to set locale to en (missing?)"
+  endtry
+  set encoding&
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/test_gettext_utf8.vim b/src/testdir/test_gettext_utf8.vim
new file mode 100644
index 0000000..277710e
--- /dev/null
+++ b/src/testdir/test_gettext_utf8.vim
@@ -0,0 +1,22 @@
+source check.vim
+
+" Test for gettext()
+func Test_gettext()
+  set encoding=utf-8
+  call bindtextdomain("__PACKAGE__", getcwd())
+  try
+    language ru_RU
+    call assert_equal('ОШИБКА: ', gettext("ERROR: ", "__PACKAGE__"))
+  catch /^Vim\%((\a\+)\)\=:E197:/
+    throw "Skipped: not possible to set locale to ru (missing?)"
+  endtry
+  try
+    language en_GB.UTF-8
+    call assert_equal('ERROR: ', gettext("ERROR: ", "__PACKAGE__"))
+  catch /^Vim\%((\a\+)\)\=:E197:/
+    throw "Skipped: not possible to set locale to en (missing?)"
+  endtry
+  set encoding&
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index f9c2983..5f9f2b0 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    509,
+/**/
     508,
 /**/
     507,