git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH/RFC v5 1/2] Add infrastructure for translating Git with gettext

From
Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Date
Jun 2, 2010, 22:33 UTC
Message-ID
<1275517993-23093-2-git-send-email-avarab@gmail.com>
In-Reply-To
<AANLkTimhxqGRPjYz-k5f-LTIk6KajEwUuGJWt-dAKZQT@mail.gmail.com>

All of the interface messages in Git core are currently hardcoded in English. Change that by optionally enabling translation of the core C, Shell and Perl programs via GNU gettext. If you set the appropriate LC_* variables Git will speak your language, provided that someone has submitted a translation.

If gettext isn't available, or if Git is compiled with NO_GETTEXT=YesPlease, then Git fall back on its previous behavior of only speaking English.

With NO_GETTEXT=YesPlease gettext support will be #defined away for C programs. For Shell and Perl programs we rely on the git message catalog not being avalalable. That's a reasonable assumption since the *.po files won't be installed on the system during make install.

The gettext wrappers that are provided in the patch are only the bare minimum required to begin translation work. In particular I haven't added wrappers for the gettext functions that enable plural support, or those that provide message context (msgctxt). Those can be added later. The intent is to start with a small subset and see what we need later, not to start with something that's unnecessarily large right away.

Implementation and usage notes:
 * General:
   Gettext .mo files will be installed and looked for in the standard
   $(prefix)/share/locale path. GIT_TEXTDOMAINDIR can also be set to
   override that. But this is only intended to be used to test Git
   itself.
 * Perl:
   Perl code that wants to be localized should use the new Git::I18n
   module. It imports a __ function into the caller's package by
   default.
   Instead of using the high level Locale::TextDomain interface I've
   opted to use the low-level (equivalent to the C interface)
   Locale::Messages module, which Locale::TextDomain itself uses.
   Locale::TextDomain does a lot of redundant work we don't need, and
   some of it would potentially introduce bugs. It tries to set the
   $TEXTDOMAIN based on package of the caller, and has its own
   hardcoded paths where it'll search for messages.
   I found it easier just to completely avoid it rather than try to
   circumvent its behavior. In any case, this is an issue wholly
   internal Git::I18N. Its guts can be changed later if that's deemed
   necessary.
 * Shell:
   Shell code that wants to be localized should use the new
   git-sh-i18n library. It's just a wrapper for the system's
   gettext.sh.
   If gettext.sh isn't available we'll fall back on a dumb
   printf(1)-powered fall-through wrapper.
   I originally tried to detect if the system supported `echo -n' but
   I found this to be a waste of time. My benchmarks on Linux, Solaris
   and FreeBSD reveal that printf(1) is fast enough, especially since
   we aren't calling gettext() from within any tight loops.

This patch is based on work by Jeff Epler <jepler@unpythonic.net> who did the initial Makefile / C work, and a lot of comments from the Git mailing list.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore                         |    2 +
 Makefile                           |   44 ++++++++++++++-
 config.mak.in                      |    1 +
 configure.ac                       |    6 ++
 gettext.c                          |   25 +++++++++
 gettext.h                          |   13 +++++
 git-sh-i18n.sh                     |   33 +++++++++++
 git.c                              |    3 +
 perl/Git/I18N.pm                   |   77 ++++++++++++++++++++++++++
 perl/Makefile                      |    3 +-
 perl/Makefile.PL                   |   14 +++++-
 po/.gitignore                      |    1 +
 po/is.po                           |   48 ++++++++++++++++
 t/t0200-gettext.sh                 |  104 ++++++++++++++++++++++++++++++++++++
 t/t0200-gettext/test.c             |   11 ++++
 t/t0200-gettext/test.perl          |   11 ++++
 t/t0200-gettext/test.sh            |   14 +++++
 t/t0201-gettext-shell-fallbacks.sh |   40 ++++++++++++++
 t/test-lib.sh                      |    1 +
 19 files changed, 447 insertions(+), 4 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 create mode 100644 git-sh-i18n.sh
 create mode 100644 perl/Git/I18N.pm
 create mode 100644 po/.gitignore
 create mode 100644 po/is.po
 create mode 100755 t/t0200-gettext.sh
 create mode 100644 t/t0200-gettext/test.c
 create mode 100644 t/t0200-gettext/test.perl
 create mode 100644 t/t0200-gettext/test.sh
 create mode 100755 t/t0201-gettext-shell-fallbacks.sh
diff --git a/.gitignore b/.gitignore
index 14e2b6b..6c2b193 100644
--- a/.gitignore
+++ b/.gitignore
@@ -125,6 +125,7 @@
 /git-rm
 /git-send-email
 /git-send-pack
+/git-sh-i18n
 /git-sh-setup
 /git-shell
 /git-shortlog
@@ -204,3 +205,4 @@
 *.pdb
 /Debug/
 /Release/
+/share/
diff --git a/Makefile b/Makefile
index d5d6565..32f5e52 100644
--- a/Makefile
+++ b/Makefile
@@ -272,6 +272,7 @@ mandir = share/man
 infodir = share/info
 gitexecdir = libexec/git-core
 sharedir = $(prefix)/share
+localedir = $(sharedir)/locale
 template_dir = share/git-core/templates
 htmldir = share/doc/git-doc
 ifeq ($(prefix),/usr)
@@ -285,7 +286,7 @@ lib = lib
 # DESTDIR=
 pathsep = :
 
-export prefix bindir sharedir sysconfdir
+export prefix bindir sharedir sysconfdir localedir
 
 CC = gcc
 AR = ar
@@ -297,6 +298,8 @@ RPMBUILD = rpmbuild
 TCL_PATH = tclsh
 TCLTK_PATH = wish
 PTHREAD_LIBS = -lpthread
+XGETTEXT = xgettext
+MSGFMT = msgfmt
 
 export TCL_PATH TCLTK_PATH
 
@@ -358,6 +361,7 @@ SCRIPT_SH += git-web--browse.sh
 SCRIPT_LIB += git-mergetool--lib
 SCRIPT_LIB += git-parse-remote
 SCRIPT_LIB += git-sh-setup
+SCRIPT_LIB += git-sh-i18n
 
 SCRIPT_PERL += git-add--interactive.perl
 SCRIPT_PERL += git-difftool.perl
@@ -523,6 +527,7 @@ LIB_H += userdiff.h
 LIB_H += utf8.h
 LIB_H += xdiff-interface.h
 LIB_H += xdiff/xdiff.h
+LIB_H += gettext.h
 
 LIB_OBJS += abspath.o
 LIB_OBJS += advice.o
@@ -564,6 +569,7 @@ LIB_OBJS += entry.o
 LIB_OBJS += environment.o
 LIB_OBJS += exec_cmd.o
 LIB_OBJS += fsck.o
+LIB_OBJS += gettext.o
 LIB_OBJS += graph.o
 LIB_OBJS += grep.o
 LIB_OBJS += hash.o
@@ -1386,6 +1392,12 @@ ifdef USE_NED_ALLOCATOR
        COMPAT_OBJS += compat/nedmalloc/nedmalloc.o
 endif
 
+ifdef NO_GETTEXT
+	COMPAT_CFLAGS += -DNO_GETTEXT
+else
+	LIBINTL = -lintl
+endif
+
 ifeq ($(TCLTK_PATH),)
 NO_TCLTK=NoThanks
 endif
@@ -1415,6 +1427,7 @@ ifndef V
 	QUIET_BUILT_IN = @echo '   ' BUILTIN $@;
 	QUIET_GEN      = @echo '   ' GEN $@;
 	QUIET_LNCP     = @echo '   ' LN/CP $@;
+	QUIET_MSGFMT   = @echo '   ' MSGFMT $@;
 	QUIET_SUBDIR0  = +@subdir=
 	QUIET_SUBDIR1  = ;$(NO_SUBDIR) echo '   ' SUBDIR $$subdir; \
 			 $(MAKE) $(PRINT_DIR) -C $$subdir
@@ -1442,7 +1455,9 @@ gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
 template_dir_SQ = $(subst ','\'',$(template_dir))
 htmldir_SQ = $(subst ','\'',$(htmldir))
 prefix_SQ = $(subst ','\'',$(prefix))
+sharedir_SQ = $(subst ','\'',$(sharedir))
 
+LOCALEDIR_SQ = $(subst ','\'',$(localedir))
 SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
 PYTHON_PATH_SQ = $(subst ','\'',$(PYTHON_PATH))
@@ -1491,7 +1506,7 @@ ifndef NO_TCLTK
 	$(QUIET_SUBDIR0)gitk-git $(QUIET_SUBDIR1) all
 endif
 ifndef NO_PERL
-	$(QUIET_SUBDIR0)perl $(QUIET_SUBDIR1) PERL_PATH='$(PERL_PATH_SQ)' prefix='$(prefix_SQ)' all
+	$(QUIET_SUBDIR0)perl $(QUIET_SUBDIR1) PERL_PATH='$(PERL_PATH_SQ)' prefix='$(prefix_SQ)' localedir='$(localedir_SQ)' all
 endif
 ifndef NO_PYTHON
 	$(QUIET_SUBDIR0)git_remote_helpers $(QUIET_SUBDIR1) PYTHON_PATH='$(PYTHON_PATH_SQ)' prefix='$(prefix_SQ)' all
@@ -1536,6 +1551,7 @@ $(RM) $@ $@+ && \
 sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
     -e 's|@SHELL_PATH@|$(SHELL_PATH_SQ)|' \
     -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
+    -e 's|@@LOCALEDIR@@|$(LOCALEDIR_SQ)|g' \
     -e 's/@@NO_CURL@@/$(NO_CURL)/g' \
     -e $(BROKEN_PATH_FIX) \
     $@.sh >$@+
@@ -1868,6 +1884,21 @@ cscope:
 	$(RM) cscope*
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
+pot:
+	$(XGETTEXT) --add-comments --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c) t/t0200-gettext/test.c
+	$(XGETTEXT) --add-comments --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH) t/t0200-gettext/test.sh
+	$(XGETTEXT) --add-comments --join-existing --keyword=__ --output=po/git.pot --language=Perl $(SCRIPT_PERL) t/t0200-gettext/test.perl
+
+POFILES := $(wildcard po/*.po)
+MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
+MODIRS := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/,$(POFILES))
+ifndef NO_GETTEXT
+all:: $(MOFILES)
+endif
+share/locale/%/LC_MESSAGES/git.mo: po/%.po
+	@mkdir -p $(dir $@)
+	$(QUIET_MSGFMT)$(MSGFMT) -o $@ $<
+
 ### Detect prefix changes
 TRACK_CFLAGS = $(subst ','\'',$(ALL_CFLAGS)):\
              $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ)
@@ -1980,6 +2011,11 @@ install: all
 	$(INSTALL) $(ALL_PROGRAMS) '$(DESTDIR_SQ)$(gitexec_instdir_SQ)'
 	$(INSTALL) -m 644 $(SCRIPT_LIB) '$(DESTDIR_SQ)$(gitexec_instdir_SQ)'
 	$(INSTALL) $(install_bindir_programs) '$(DESTDIR_SQ)$(bindir_SQ)'
+ifndef NO_GETTEXT
+	$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(sharedir_SQ)/locale'
+	(cd share && tar cf - locale) | \
+		(cd '$(DESTDIR_SQ)$(sharedir_SQ)' && umask 022 && tar xof -)
+endif
 	$(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install
 ifndef NO_PERL
 	$(MAKE) -C perl prefix='$(prefix_SQ)' DESTDIR='$(DESTDIR_SQ)' install
@@ -2127,6 +2163,10 @@ ifndef NO_TCLTK
 	$(MAKE) -C git-gui clean
 endif
 	$(RM) GIT-VERSION-FILE GIT-CFLAGS GIT-GUI-VARS GIT-BUILD-OPTIONS
+ifndef NO_GETTEXT
+	$(RM) po/git.pot
+	$(RM) -r share/
+endif
 
 .PHONY: all install clean strip
 .PHONY: shell_compatibility_test please_set_SHELL_PATH_to_a_more_modern_shell
diff --git a/config.mak.in b/config.mak.in
index 0d4b64d..a15f3c1 100644
--- a/config.mak.in
+++ b/config.mak.in
@@ -32,6 +32,7 @@ NO_CURL=@NO_CURL@
 NO_EXPAT=@NO_EXPAT@
 NO_LIBGEN_H=@NO_LIBGEN_H@
 HAVE_PATHS_H=@HAVE_PATHS_H@
+NO_GETTEXT=@NO_GETTEXT@
 NEEDS_LIBICONV=@NEEDS_LIBICONV@
 NEEDS_SOCKET=@NEEDS_SOCKET@
 NEEDS_RESOLV=@NEEDS_RESOLV@
diff --git a/configure.ac b/configure.ac
index 71038fc..7bebfd8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -730,6 +730,12 @@ AC_CHECK_HEADER([paths.h],
 [HAVE_PATHS_H=])
 AC_SUBST(HAVE_PATHS_H)
 #
+# Define NO_GETTEXT if you don't have libintl.h
+AC_CHECK_HEADER([libintl.h],
+[NO_GETTEXT=],
+[NO_GETTEXT=YesPlease])
+AC_SUBST(NO_GETTEXT)
+#
 # Define NO_STRCASESTR if you don't have strcasestr.
 GIT_CHECK_FUNC(strcasestr,
 [NO_STRCASESTR=],
diff --git a/gettext.c b/gettext.c
new file mode 100644
index 0000000..22cdcc1
--- /dev/null
+++ b/gettext.c
@@ -0,0 +1,25 @@
+#ifdef NO_GETTEXT
+void git_setup_gettext(void) {}
+#else
+#include "exec_cmd.h"
+#include <libintl.h>
+#include <stdlib.h>
+
+void git_setup_gettext(void) {
+	char *podir;
+	char *envdir = getenv("GIT_TEXTDOMAINDIR");
+
+	if (envdir) {
+		(void)bindtextdomain("git", envdir);
+	} else {
+		podir = (char *)system_path("share/locale");
+		if (!podir) return;
+		(void)bindtextdomain("git", podir);
+		free(podir);
+	}
+
+	(void)setlocale(LC_MESSAGES, "");
+	(void)setlocale(LC_CTYPE, "");
+	(void)textdomain("git");
+}
+#endif
diff --git a/gettext.h b/gettext.h
new file mode 100644
index 0000000..a99da6a
--- /dev/null
+++ b/gettext.h
@@ -0,0 +1,13 @@
+#ifndef GETTEXT_H
+#define GETTEXT_H
+
+void git_setup_gettext(void);
+
+#ifdef NO_GETTEXT
+#define _(s) (s)
+#else
+#include <libintl.h>
+#define _(s) gettext(s)
+#endif
+
+#endif
diff --git a/git-sh-i18n.sh b/git-sh-i18n.sh
new file mode 100644
index 0000000..ae1d5cd
--- /dev/null
+++ b/git-sh-i18n.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+#
+# This is what Git uses internally to bootstrap its gettext
+# shellscript interface.
+
+# Try to use libintl's gettext.sh, or fall back to English if we
+# can't.
+. gettext.sh
+
+if test $? -eq 0 && test -z "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS"
+then
+	TEXTDOMAIN=git
+	export TEXTDOMAIN
+	if [ -z "$GIT_TEXTDOMAINDIR" ]
+	then
+		TEXTDOMAINDIR="@@LOCALEDIR@@"
+	else
+		TEXTDOMAINDIR="$GIT_TEXTDOMAINDIR"
+	fi
+	export TEXTDOMAINDIR
+else
+	# Since gettext.sh isn't available we'll have to define our own
+	# dummy pass-through functions.
+
+	gettext () {
+		printf "%s" "$1"
+	}
+
+	eval_gettext () {
+		gettext_eval="printf '%s' \"$1\""
+		printf "%s" "`eval \"$gettext_eval\"`"
+	}
+fi
diff --git a/git.c b/git.c
index 99f0363..d749eab 100644
--- a/git.c
+++ b/git.c
@@ -3,6 +3,7 @@
 #include "cache.h"
 #include "quote.h"
 #include "run-command.h"
+#include "gettext.h"
 
 const char git_usage_string[] =
 	"git [--version] [--exec-path[=GIT_EXEC_PATH]] [--html-path]\n"
@@ -490,6 +491,8 @@ int main(int argc, const char **argv)
 	if (!cmd)
 		cmd = "git-help";
 
+	git_setup_gettext();
+
 	/*
 	 * "git-xxxx" is the same as "git xxxx", but we obviously:
 	 *
diff --git a/perl/Git/I18N.pm b/perl/Git/I18N.pm
new file mode 100644
index 0000000..cabdcec
--- /dev/null
+++ b/perl/Git/I18N.pm
@@ -0,0 +1,77 @@
+package Git::I18N;
+use strict;
+use warnings;
+use Exporter;
+use base 'Exporter';
+
+our $VERSION = '0.01';
+
+our @EXPORT = qw(__);
+our @EXPORT_OK = @EXPORT;
+
+sub __ ($);
+
+sub __bootstrap_locale_messages {
+	our $TEXTDOMAIN = 'git';
+	our $TEXTDOMAINDIR = $ENV{GIT_TEXTDOMAINDIR} || '++LOCALEDIR++';
+
+	require POSIX;
+	POSIX->import(qw(setlocale));
+	# Non-core prerequisite module
+	require Locale::Messages;
+	Locale::Messages->import(qw(:locale_h :libintl_h));
+
+	setlocale(LC_MESSAGES(), '');
+	setlocale(LC_CTYPE(), '');
+	textdomain($TEXTDOMAIN);
+	bindtextdomain($TEXTDOMAIN => $TEXTDOMAINDIR);
+
+	return;
+}
+
+BEGIN
+{
+	local ($@, $!);
+	eval { __bootstrap_locale_messages() };
+	if ($@) {
+		# Oh noes, no Locale::Messages here
+		*__	 = sub ($) { $_[0] };
+	} else {
+		*__	 = sub ($) { goto &Locale::Messages::gettext };
+	}
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Git::I18N - Perl interface to Git's Gettext localizations
+
+=head1 SYNOPSIS
+
+	use Git::I18N;
+
+	print __("Welcome to Git!\n");
+
+	printf __("The following error occured: %s\n"), $error;
+
+=head1 DESCRIPTION
+
+Git's internal interface to Gettext via L<Locale::Messages>. If
+L<Locale::Messages> can't be loaded (it's not a core module) we
+provide stub passthrough fallbacks.
+
+=head1 FUNCTIONS
+
+=head2 __($)
+
+L<Locale::Messages>'s gettext function if all goes well, otherwise our
+passthrough fallback function.
+
+=head1 AUTHOR
+
+E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
+
+=cut
diff --git a/perl/Makefile b/perl/Makefile
index 4ab21d6..4e624ff 100644
--- a/perl/Makefile
+++ b/perl/Makefile
@@ -5,6 +5,7 @@ makfile:=perl.mak
 
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
 prefix_SQ = $(subst ','\'',$(prefix))
+localedir_SQ = $(subst ','\'',$(localedir))
 
 ifndef V
 	QUIET = @
@@ -38,7 +39,7 @@ $(makfile): ../GIT-CFLAGS Makefile
 	echo '	echo $(instdir_SQ)' >> $@
 else
 $(makfile): Makefile.PL ../GIT-CFLAGS
-	$(PERL_PATH) $< PREFIX='$(prefix_SQ)'
+	$(PERL_PATH) $< PREFIX='$(prefix_SQ)' --localedir='$(localedir_SQ)'
 endif
 
 # this is just added comfort for calling make directly in perl dir
diff --git a/perl/Makefile.PL b/perl/Makefile.PL
index 0b9deca..456d45b 100644
--- a/perl/Makefile.PL
+++ b/perl/Makefile.PL
@@ -1,4 +1,12 @@
+use strict;
+use warnings;
 use ExtUtils::MakeMaker;
+use Getopt::Long;
+
+# Sanity: die at first unknown option
+Getopt::Long::Configure qw/ pass_through /;
+
+GetOptions("localedir=s" => \my $localedir);
 
 sub MY::postamble {
 	return <<'MAKE_FRAG';
@@ -16,7 +24,10 @@ endif
 MAKE_FRAG
 }
 
-my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm');
+my %pm = (
+	'Git.pm' => '$(INST_LIBDIR)/Git.pm',
+	'Git/I18N.pm' => '$(INST_LIBDIR)/Git/I18N.pm',
+);
 
 # We come with our own bundled Error.pm. It's not in the set of default
 # Perl modules so install it if it's not available on the system yet.
@@ -33,6 +44,7 @@ WriteMakefile(
 	NAME            => 'Git',
 	VERSION_FROM    => 'Git.pm',
 	PM		=> \%pm,
+	PM_FILTER	=> qq[\$(PERL) -pe "s<\\Q++LOCALEDIR++\\E><$localedir>"],
 	MAKEFILE	=> 'perl.mak',
 	INSTALLSITEMAN3DIR => '$(SITEPREFIX)/share/man/man3'
 );
diff --git a/po/.gitignore b/po/.gitignore
new file mode 100644
index 0000000..221000e
--- /dev/null
+++ b/po/.gitignore
@@ -0,0 +1 @@
+/*.pot
diff --git a/po/is.po b/po/is.po
new file mode 100644
index 0000000..ac4317b
--- /dev/null
+++ b/po/is.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Git\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-06-02 17:35+0000\n"
+"PO-Revision-Date: 2010-06-02 16:01+0000\n"
+"Last-Translator: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n"
+"Language-Team: Git Mailing List <git@vger.kernel.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Language: English\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#. TRANSLATORS: This is a test. You don't need to translate it.
+#: t/t0200-gettext/test.c:6
+msgid "TEST: A C test string"
+msgstr "TILRAUN: C tilraunastrengur"
+
+#. TRANSLATORS: This is a test. You don't need to translate it.
+#: t/t0200-gettext/test.c:9
+#, c-format
+msgid "TEST: A C test string %s"
+msgstr "TILRAUN: C tilraunastrengur %s"
+
+#. TRANSLATORS: This is a test. You don't need to translate it.
+#: t/t0200-gettext/test.sh:8
+msgid "TEST: A Shell test string"
+msgstr "TILRAUN: Skeljartilraunastrengur"
+
+#. TRANSLATORS: This is a test. You don't need to translate it.
+#: t/t0200-gettext/test.sh:11
+#, sh-format
+msgid "TEST: A Shell test $variable"
+msgstr "TILRAUN: Skeljartilraunastrengur með breytunni $variable"
+
+#. This is a phony Perl program that's only here to test xgettext
+#. message extraction
+#. TRANSLATORS: This is a test. You don't need to translate it.
+#: t/t0200-gettext/test.perl:5
+msgid "TEST: A Perl test string"
+msgstr "TILRAUN: Perl tilraunastrengur"
+
+#. TRANSLATORS: This is a test. You don't need to translate it.
+#: t/t0200-gettext/test.perl:8
+#, perl-format
+msgid "TEST: A Perl test variable %s"
+msgstr "TILRAUN: Perl tilraunastrengur með breytunni %s"
diff --git a/t/t0200-gettext.sh b/t/t0200-gettext.sh
new file mode 100755
index 0000000..7b01ce4
--- /dev/null
+++ b/t/t0200-gettext.sh
@@ -0,0 +1,104 @@
+#!/bin/sh
+D=`pwd`
+GIT_TEXTDOMAINDIR="$D/../share/locale"
+GIT_POROOT="$D/../po"
+export GIT_TEXTDOMAINDIR GIT_POROOT
+
+test_description='Gettext support for Git'
+. ./test-lib.sh
+. ../../git-sh-i18n
+
+test_expect_success 'sanity: $TEXTDOMAIN is git' '
+    test $TEXTDOMAIN = "git"
+'
+
+test_expect_success 'sanity: $TEXTDOMAINDIR exists' '
+    test -d "$TEXTDOMAINDIR" &&
+    test "$TEXTDOMAINDIR" = "$GIT_TEXTDOMAINDIR"
+'
+
+test_expect_success 'sanity: Icelandic locale was compiled' '
+    test -f "$TEXTDOMAINDIR/is/LC_MESSAGES/git.mo"
+'
+
+test_expect_success 'sanity: No gettext("") data for fantasy locale' '
+    LANGUAGE=is LC_ALL=tlh_TLH.UTF-8 gettext "" > real-locale &&
+    test_expect_failure test -s real-locale
+'
+
+test_expect_success 'sanity: Some gettext("") data for real locale' '
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "" > fantasy-locale &&
+    test -s fantasy-locale
+'
+
+# TODO: When we have more locales, generalize this to test them
+# all. Maybe we'll need a dir->locale map for that.
+test_expect_success 'sanity: gettext("") metadata is OK' '
+    # Return value may be non-zero
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "" > zero-expect &&
+    grep "Project-Id-Version: Git" zero-expect &&
+    grep "Git Mailing List <git@vger.kernel.org>" zero-expect &&
+    grep "Content-Type: text/plain; charset=UTF-8" zero-expect &&
+    grep "Content-Transfer-Encoding: 8bit" zero-expect
+'
+
+test_expect_success 'sanity: gettext(unknown) is passed through' '
+    printf "This is not a translation string"  > expect &&
+    gettext "This is not a translation string" > actual &&
+    eval_gettext "This is not a translation string" > actual &&
+    test_cmp expect actual
+'
+
+# xgettext from C
+test_expect_success 'xgettext: C extraction' '
+    printf "TILRAUN: C tilraunastrengur" > expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "TEST: A C test string" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'xgettext: C extraction with %s' '
+    printf "TILRAUN: C tilraunastrengur %%s" > expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "TEST: A C test string %s" > actual &&
+    test_cmp expect actual
+'
+
+# xgettext from Shell
+test_expect_success 'xgettext: Shell extraction' '
+    printf "TILRAUN: Skeljartilraunastrengur" > expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "TEST: A Shell test string" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'xgettext: Shell extraction with $variable' '
+    printf "TILRAUN: Skeljartilraunastrengur með breytunni a var i able" > x-expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 variable="a var i able" eval_gettext "TEST: A Shell test \$variable" > x-actual &&
+    test_cmp x-expect x-actual
+'
+
+# xgettext from Perl
+test_expect_success 'xgettext: Perl extraction' '
+    printf "TILRAUN: Perl tilraunastrengur" > expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "TEST: A Perl test string" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'xgettext: Perl extraction with %s' '
+    printf "TILRAUN: Perl tilraunastrengur með breytunni %%s" > expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 gettext "TEST: A Perl test variable %s" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'xgettext: Perl _() strings are not extracted' '
+    > expect &&
+    grep "A Perl string xgettext will not get" $GIT_POROOT/is.po > actual;
+    test_cmp expect actual
+'
+
+# xgettext extracts comments
+test_expect_success 'xgettext: Comment extraction with --add-comments' '
+    grep "TRANSLATORS: This is a test" ../t0200-gettext/* | wc -l > expect &&
+    grep "TRANSLATORS: This is a test" $GIT_POROOT/is.po  | wc -l > actual &&
+    test_cmp expect actual
+'
+
+test_done
diff --git a/t/t0200-gettext/test.c b/t/t0200-gettext/test.c
new file mode 100644
index 0000000..9fa4c23
--- /dev/null
+++ b/t/t0200-gettext/test.c
@@ -0,0 +1,11 @@
+/* This is a phony C program that's only here to test xgettext message extraction */
+
+int main(void)
+{
+	/* TRANSLATORS: This is a test. You don't need to translate it. */
+	puts(_("TEST: A C test string"));
+
+	/* TRANSLATORS: This is a test. You don't need to translate it. */
+	printf(_("TEST: A C test string %s"), "variable");
+}
+	
diff --git a/t/t0200-gettext/test.perl b/t/t0200-gettext/test.perl
new file mode 100644
index 0000000..8c50ef0
--- /dev/null
+++ b/t/t0200-gettext/test.perl
@@ -0,0 +1,11 @@
+# This is a phony Perl program that's only here to test xgettext
+# message extraction
+
+# TRANSLATORS: This is a test. You don't need to translate it.
+print __("TEST: A Perl test string");
+
+# TRANSLATORS: This is a test. You don't need to translate it.
+printf __("TEST: A Perl test variable %s"), "moo";
+
+# TRANSLATORS: If you see this, Git has a bug
+print _"TEST: A Perl string xgettext will not get";
diff --git a/t/t0200-gettext/test.sh b/t/t0200-gettext/test.sh
new file mode 100644
index 0000000..022d607
--- /dev/null
+++ b/t/t0200-gettext/test.sh
@@ -0,0 +1,14 @@
+# This is a phony Shell program that's only here to test xgettext
+# message extraction
+
+# so the above comment won't be folded into the next one by xgettext
+echo
+
+# TRANSLATORS: This is a test. You don't need to translate it.
+gettext "TEST: A Shell test string"
+
+# TRANSLATORS: This is a test. You don't need to translate it.
+eval_gettext "TEST: A Shell test \$variable"
+
+# TRANSLATORS: If you see this, Git has a bug
+_("TEST: A Shell string xgettext won't get")
diff --git a/t/t0201-gettext-shell-fallbacks.sh b/t/t0201-gettext-shell-fallbacks.sh
new file mode 100755
index 0000000..827adb2
--- /dev/null
+++ b/t/t0201-gettext-shell-fallbacks.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+D=`pwd`
+GIT_TEXTDOMAINDIR="$D/../share/locale"
+GIT_INTERNAL_GETTEXT_TEST_FALLBACKS=YesPlease
+
+export GIT_TEXTDOMAINDIR GIT_INTERNAL_GETTEXT_TEST_FALLBACKS
+
+test_description='Gettext Shell fallbacks'
+. ./test-lib.sh
+. ../../git-sh-i18n
+
+test_expect_success 'sanity: $GIT_INTERNAL_GETTEXT_TEST_FALLBACKS is set' '
+	test_expect_failure test -z "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS"
+'
+
+test_expect_success 'gettext: our gettext() fallback has pass-through semantics' '
+    printf "test" > expect &&
+    gettext "test" > actual &&
+    test_cmp expect actual &&
+    printf "test more words" > expect &&
+    gettext "test more words" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'eval_gettext: our eval_gettext() fallback has pass-through semantics' '
+    printf "test" > expect &&
+    eval_gettext "test" > actual &&
+    test_cmp expect actual &&
+    printf "test more words" > expect &&
+    eval_gettext "test more words" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'eval_gettext: our eval_gettext() fallback can interpolate variables' '
+    printf "test YesPlease" > expect &&
+    eval_gettext "test \$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" > actual &&
+    test_cmp expect actual
+'
+
+test_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 454880a..cb3c9c5 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -37,6 +37,7 @@ ORIGINAL_TERM=$TERM
 # For repeatability, reset the environment to known value.
 LANG=C
 LC_ALL=C
+LANGUAGE=C
 PAGER=cat
 TZ=UTC
 TERM=dumb
-- 
1.7.1.242.g290e3.dirty
Previous: Ævar Arnfjörð BjarmasonNext: Ævar Arnfjörð Bjarmason
Message 44 of 56 in “Add internationalization support to Git”
  1. 0/5 Add internationalization support to GitÆvar Arnfjörð Bjarmason, May 29, 2010
  2. 1/5 Add infrastructure for translating Git with gettextÆvar Arnfjörð Bjarmason, May 29, 2010
  3. 2/5 gitignore: Ignore files generated by gettextÆvar Arnfjörð Bjarmason, May 29, 2010
  4. 3/5 Makefile: Remove Gettext files on make cleanÆvar Arnfjörð Bjarmason, May 29, 2010
  5. 4/5 gettext: Add a skeleton po/is.poÆvar Arnfjörð Bjarmason, May 29, 2010
  6. 5/5 Add infrastructure to make shellscripts translatableÆvar Arnfjörð Bjarmason, May 29, 2010
  7. Jonathan NiederMay 30, 2010
  8. Ævar Arnfjörð BjarmasonMay 30, 2010
  9. Jonathan NiederMay 30, 2010
  10. Ævar Arnfjörð BjarmasonMay 31, 2010
  11. 0/6 Add internationalization support to GitÆvar Arnfjörð Bjarmason, May 30, 2010
  12. 1/6 Add infrastructure for translating Git with gettextÆvar Arnfjörð Bjarmason, May 30, 2010
  13. Jakub NarebskiJun 1, 2010
  14. autoconf: Check if <libintl.h> exists and set NO_GETTEXTÆvar Arnfjörð Bjarmason, Jun 1, 2010
  15. Jakub NarebskiJun 1, 2010
  16. 2/6 gitignore: Ignore files generated by gettextÆvar Arnfjörð Bjarmason, May 30, 2010
  17. 3/6 Makefile: Remove Gettext files on make cleanÆvar Arnfjörð Bjarmason, May 30, 2010
  18. 4/6 gettext: Add a Gettext interface for shell scriptsÆvar Arnfjörð Bjarmason, May 30, 2010
  19. 5/6 gettext: Add a Gettext interface for PerlÆvar Arnfjörð Bjarmason, May 30, 2010
  20. Jakub NarebskiJun 1, 2010
  21. Ævar Arnfjörð BjarmasonJun 1, 2010
  22. Jakub NarebskiJun 2, 2010
  23. 6/6 gettext: Add a skeleton po/is.poÆvar Arnfjörð Bjarmason, May 30, 2010
  24. Jonathan NiederMay 30, 2010
  25. Jonathan NiederMay 30, 2010
  26. Ævar Arnfjörð BjarmasonMay 31, 2010
  27. 0/7 Add internationalization support to GitÆvar Arnfjörð Bjarmason, Jun 1, 2010
  28. Ævar Arnfjörð BjarmasonJun 2, 2010
  29. 0/7 Add internationalization support to GitÆvar Arnfjörð Bjarmason, Jun 2, 2010
  30. 1/7 Add infrastructure for translating Git with gettextÆvar Arnfjörð Bjarmason, Jun 2, 2010
  31. Peter KreftingJun 2, 2010
  32. Ævar Arnfjörð BjarmasonJun 2, 2010
  33. Peter KreftingJun 2, 2010
  34. Ævar Arnfjörð BjarmasonJun 2, 2010
  35. Peter KreftingJun 2, 2010
  36. 2/7 gettext: Add a Gettext interface for shell scriptsÆvar Arnfjörð Bjarmason, Jun 2, 2010
  37. 3/7 gettext: Add a Gettext interface for PerlÆvar Arnfjörð Bjarmason, Jun 2, 2010
  38. 4/7 Makefile: Don't install Gettext .mo files if NO_GETTEXTÆvar Arnfjörð Bjarmason, Jun 2, 2010
  39. 5/7 Makefile: Override --keyword= for all languagesÆvar Arnfjörð Bjarmason, Jun 2, 2010
  40. 6/7 gettext: Sanity tests for Git's Gettext supportÆvar Arnfjörð Bjarmason, Jun 2, 2010
  41. 7/7 gettext: Add a skeleton po/is.poÆvar Arnfjörð Bjarmason, Jun 2, 2010
  42. Johannes SixtJun 2, 2010
  43. 0/2 Add infrastructure for translating Git with gettextÆvar Arnfjörð Bjarmason, Jun 2, 2010
  44. 1/2 Add infrastructure for translating Git with gettextÆvar Arnfjörð Bjarmason, Jun 2, 2010
  45. 2/2 Add initial C, Shell and Perl gettext translationsÆvar Arnfjörð Bjarmason, Jun 2, 2010
  46. 1/7 Add infrastructure for translating Git with gettextÆvar Arnfjörð Bjarmason, Jun 1, 2010
  47. 2/7 gettext: Add a Gettext interface for shell scriptsÆvar Arnfjörð Bjarmason, Jun 1, 2010
  48. Johannes SixtJun 2, 2010
  49. Ævar Arnfjörð BjarmasonJun 2, 2010
  50. Johannes SixtJun 2, 2010
  51. 3/7 gettext: Add a Gettext interface for PerlÆvar Arnfjörð Bjarmason, Jun 1, 2010
  52. 4/7 Makefile: Don't install Gettext .mo files if NO_GETTEXTÆvar Arnfjörð Bjarmason, Jun 1, 2010
  53. 5/7 Makefile: Override --keyword= for all languagesÆvar Arnfjörð Bjarmason, Jun 1, 2010
  54. 6/7 gettext: Basic sanity tests for Git's Gettext supportÆvar Arnfjörð Bjarmason, Jun 1, 2010
  55. Johannes SixtJun 2, 2010
  56. 7/7 gettext: Add a skeleton po/is.poÆvar Arnfjörð Bjarmason, Jun 1, 2010

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.