threads / rfc / 23941

RFC patch, 5 partsAdd internationalization support to Git

Subject: [PATCH/RFC 0/5] Add internationalization support to Git

## tl;dr

56 messages between May 29, 2010 and Jun 2, 2010. Diffs are folded; open one to read it.

replies: 55people: 5as markdown or json

Ævar Arnfjörð Bjarmason· May 29, 2010, 22:45 UTC · lore

Here's an updated version of the patch series begun by Jeff Epler to make Git optionally translatable with GNU gettext. I'm submitting it now because I'd like to get some comments on the approach, and specifically whether something like this is likely to be accepted into Git.

Changes since the initial submission:
  * Merged in changes to wt-status.c from the master branch
  * Made Gettext play nice with the build system (.gitignore & make
    clean)
  * Added support for making Git's shell scripts translatable by
    importing the gettext functions in git-sh-setup.sh, or supplying
    replacements if they don't work. Comments from shellscript-savy
    list members about whether the fallbacks work on odd non-GNU
    shells are especially welcome.
    I made three strings in git-pull.sh translatable as a proof of
    concept. One problem that I ran into is that xgettext(1) seems
    very particular when picking up translation strings. It accepts
    this:
        gettext "hello world"; echo
    but not this:
        die gettext "hello world"; die
    or this:
        gettext <<"END";
hello world
END
    Maybe there's a way to make it play nice. But I just used a large
    multiline string as a workaround. I don't know what to do about
    'die gettext' other than define a 'die_gettext' wrapper function
    and use `xgettext --keyword=die_gettext'.
  * There's now a skeleton is.po file. It just has one string
    translated. I can't figure out what to call any of the Git core
    concepts so I'm stuck :)
Next up:
  * Perl support. I haven't yet found what the best way of using
    libintl from Perl would be. GNU recommends Locale::TextDomain, but
    Debian only seems to have Locale::gettext which is a more
    lightweight wrapper.
    In any case I can write a wrapper for those/the shell command and
    fall back on the hardcoded strings like the C/Shell code does.

Most importantly. If there are any interested list members that want to translate Git, it's now ready for translators. You just have to add a po/$lang.po file with your favorite editor and Git's build system will install it.

One thing I haven't done is to try to go ahead and make massive changes to the Git source code to make everything translatable. Due to their nature it's a big headache to maintain changes like that if anything moves upstream.

Of course that's subject to change if other people are interested in contributing.

For reference, here's the branch on GitHub I'm hacking this in, patches welcome:

    http://github.com/avar/git/compare/master...topic/git-gettext
Jeff Epler (1):
  Add infrastructure for translating Git with gettext
Ævar Arnfjörð Bjarmason (4):
  gitignore: Ignore files generated by gettext
  Makefile: Remove Gettext files on make clean
  gettext: Add a skeleton po/is.po
  Add infrastructure to make shellscripts translatable
 .gitignore      |    1 +
 Makefile        |   35 +++++++-
 gettext.c       |   17 ++++
 gettext.h       |   15 +++
 git-pull.sh     |   15 ++--
 git-sh-setup.sh |   33 +++++++
 git.c           |    3 +
 po/.gitignore   |    1 +
 po/is.po        |  288 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 wt-status.c     |  129 +++++++++++++------------
 10 files changed, 465 insertions(+), 72 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 create mode 100644 po/.gitignore
 create mode 100644 po/is.po
Ævar Arnfjörð Bjarmason· May 29, 2010, 22:45 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH 1/5] Add infrastructure for translating Git with gettext

From: Jeff Epler <jepler@unpythonic.net>

Change the build process to use GNU's libintl if it's available. If not we define our own skeleton replacement functions which degrade gracefully to English.

Signed-off-by: Jeff Epler <jepler@unpythonic.net>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile    |   26 ++++++++++++
 gettext.c   |   17 ++++++++
 gettext.h   |   15 +++++++
 git.c       |    3 +
 wt-status.c |  129 ++++++++++++++++++++++++++++++-----------------------------
 5 files changed, 126 insertions(+), 64 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
Show changes to 5 files +126 −64

Makefile, gettext.c, gettext.h, git.c, wt-status.c

diff --git a/Makefile b/Makefile
index d5d6565..7989121 100644
--- a/Makefile
+++ b/Makefile
@@ -297,6 +297,8 @@ RPMBUILD = rpmbuild
 TCL_PATH = tclsh
 TCLTK_PATH = wish
 PTHREAD_LIBS = -lpthread
+XGETTEXT = xgettext
+MSGFMT = msgfmt
 
 export TCL_PATH TCLTK_PATH
 
@@ -523,6 +525,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 +567,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 +1390,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 +1425,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,6 +1453,7 @@ gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
 template_dir_SQ = $(subst ','\'',$(template_dir))
 htmldir_SQ = $(subst ','\'',$(htmldir))
 prefix_SQ = $(subst ','\'',$(prefix))
+sharedir_SQ = $(subst ','\'',$(sharedir))
 
 SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
@@ -1868,6 +1880,17 @@ cscope:
 	$(RM) cscope*
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
+pot:
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+
+POFILES := $(wildcard po/*.po)
+MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
+MODIRS := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/,$(POFILES))
+all:: $(MOFILES)
+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 +2003,9 @@ 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)'
+	$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(sharedir_SQ)/locale'
+	(cd share && tar cf - locale) | \
+		(cd '$(DESTDIR_SQ)$(sharedir_SQ)' && umask 022 && tar xof -)
 	$(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install
 ifndef NO_PERL
 	$(MAKE) -C perl prefix='$(prefix_SQ)' DESTDIR='$(DESTDIR_SQ)' install
diff --git a/gettext.c b/gettext.c
new file mode 100644
index 0000000..aadce19
--- /dev/null
+++ b/gettext.c
@@ -0,0 +1,17 @@
+#ifdef NO_GETTEXT
+void git_setup_gettext() {}
+#else
+#include "exec_cmd.h"
+#include <libintl.h>
+#include <stdlib.h>
+
+void git_setup_gettext() {
+    const char *podir = system_path("share/locale");
+    if(!podir) return;
+    char *ret = bindtextdomain("git", podir);
+    free((void*)podir);
+    ret = setlocale(LC_MESSAGES, "");
+    ret = setlocale(LC_CTYPE, "");
+    ret = textdomain("git");
+}
+#endif
diff --git a/gettext.h b/gettext.h
new file mode 100644
index 0000000..8b221b4
--- /dev/null
+++ b/gettext.h
@@ -0,0 +1,15 @@
+#ifndef GETTEXT_H
+#define GETTEXT_H
+
+void git_setup_gettext();
+
+#ifdef NO_GETTEXT
+#define _(s) (s)
+#define N_(s) (s)
+#else
+#include <libintl.h>
+#define _(s) gettext(s)
+#define N_(s) (s)
+#endif
+
+#endif
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/wt-status.c b/wt-status.c
index 14e0acc..70b4293 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -9,6 +9,7 @@
 #include "quote.h"
 #include "run-command.h"
 #include "remote.h"
+#include "gettext.h"
 
 static char default_wt_status_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_NORMAL, /* WT_STATUS_HEADER */
@@ -49,33 +50,33 @@ static void wt_status_print_unmerged_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Unmerged paths:");
+	color_fprintf_ln(s->fp, c, _("# Unmerged paths:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		;
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_cached_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changes to be committed:");
+	color_fprintf_ln(s->fp, c, _("# Changes to be committed:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		; /* NEEDSWORK: use "git reset --unresolve"??? */
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_dirty_header(struct wt_status *s,
@@ -84,17 +85,17 @@ static void wt_status_print_dirty_header(struct wt_status *s,
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changed but not updated:");
+	color_fprintf_ln(s->fp, c, _("# Changed but not updated:"));
 	if (!advice_status_hints)
 		return;
 	if (!has_deleted)
-		color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to update what will be committed)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add <file>...\" to update what will be committed)"));
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" to update what will be committed)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git checkout -- <file>...\" to discard changes in working directory)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" to update what will be committed)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git checkout -- <file>...\" to discard changes in working directory)"));
 	if (has_dirty_submodules)
-		color_fprintf_ln(s->fp, c, "#   (commit or discard the untracked or modified content in submodules)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (commit or discard the untracked or modified content in submodules)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_other_header(struct wt_status *s,
@@ -102,16 +103,16 @@ static void wt_status_print_other_header(struct wt_status *s,
 					 const char *how)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
-	color_fprintf_ln(s->fp, c, "# %s files:", what);
+	color_fprintf_ln(s->fp, c, _("# %s files:"), what);
 	if (!advice_status_hints)
 		return;
-	color_fprintf_ln(s->fp, c, "#   (use \"git %s <file>...\" to include in what will be committed)", how);
-	color_fprintf_ln(s->fp, c, "#");
+	color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to include in what will be committed)"), how);
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_trailer(struct wt_status *s)
 {
-	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 }
 
 #define quote_path quote_path_relative
@@ -122,20 +123,20 @@ static void wt_status_print_unmerged_data(struct wt_status *s,
 	const char *c = color(WT_STATUS_UNMERGED, s);
 	struct wt_status_change_data *d = it->util;
 	struct strbuf onebuf = STRBUF_INIT;
-	const char *one, *how = "bug";
+	const char *one, *how = _("bug");
 
 	one = quote_path(it->string, -1, &onebuf, s->prefix);
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (d->stagemask) {
-	case 1: how = "both deleted:"; break;
-	case 2: how = "added by us:"; break;
-	case 3: how = "deleted by them:"; break;
-	case 4: how = "added by them:"; break;
-	case 5: how = "deleted by us:"; break;
-	case 6: how = "both added:"; break;
-	case 7: how = "both modified:"; break;
+	case 1: how = _("both deleted:"); break;
+	case 2: how = _("added by us:"); break;
+	case 3: how = _("deleted by them:"); break;
+	case 4: how = _("added by them:"); break;
+	case 5: how = _("deleted by us:"); break;
+	case 6: how = _("both added:"); break;
+	case 7: how = _("both modified:"); break;
 	}
-	color_fprintf(s->fp, c, "%-20s%s\n", how, one);
+	color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);
 	strbuf_release(&onebuf);
 }
 
@@ -161,13 +162,13 @@ static void wt_status_print_change_data(struct wt_status *s,
 		break;
 	case WT_STATUS_CHANGED:
 		if (d->new_submodule_commits || d->dirty_submodule) {
-			strbuf_addstr(&extra, " (");
+			strbuf_addstr(&extra, _(" ("));
 			if (d->new_submodule_commits)
-				strbuf_addf(&extra, "new commits, ");
+				strbuf_addf(&extra, _("new commits, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
-				strbuf_addf(&extra, "modified content, ");
+				strbuf_addf(&extra, _("modified content, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_UNTRACKED)
-				strbuf_addf(&extra, "untracked content, ");
+				strbuf_addf(&extra, _("untracked content, "));
 			strbuf_setlen(&extra, extra.len - 2);
 			strbuf_addch(&extra, ')');
 		}
@@ -178,40 +179,40 @@ static void wt_status_print_change_data(struct wt_status *s,
 	one = quote_path(one_name, -1, &onebuf, s->prefix);
 	two = quote_path(two_name, -1, &twobuf, s->prefix);
 
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (status) {
 	case DIFF_STATUS_ADDED:
-		color_fprintf(s->fp, c, "new file:   %s", one);
+		color_fprintf(s->fp, c, _("new file:   %s"), one);
 		break;
 	case DIFF_STATUS_COPIED:
-		color_fprintf(s->fp, c, "copied:     %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("copied:     %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_DELETED:
-		color_fprintf(s->fp, c, "deleted:    %s", one);
+		color_fprintf(s->fp, c, _("deleted:    %s"), one);
 		break;
 	case DIFF_STATUS_MODIFIED:
-		color_fprintf(s->fp, c, "modified:   %s", one);
+		color_fprintf(s->fp, c, _("modified:   %s"), one);
 		break;
 	case DIFF_STATUS_RENAMED:
-		color_fprintf(s->fp, c, "renamed:    %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("renamed:    %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_TYPE_CHANGED:
-		color_fprintf(s->fp, c, "typechange: %s", one);
+		color_fprintf(s->fp, c, _("typechange: %s"), one);
 		break;
 	case DIFF_STATUS_UNKNOWN:
-		color_fprintf(s->fp, c, "unknown:    %s", one);
+		color_fprintf(s->fp, c, _("unknown:    %s"), one);
 		break;
 	case DIFF_STATUS_UNMERGED:
-		color_fprintf(s->fp, c, "unmerged:   %s", one);
+		color_fprintf(s->fp, c, _("unmerged:   %s"), one);
 		break;
 	default:
-		die("bug: unhandled diff status %c", status);
+		die(_("bug: unhandled diff status %c"), status);
 	}
 	if (extra.len) {
-		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s", extra.buf);
+		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("%s"), extra.buf);
 		strbuf_release(&extra);
 	}
-	fprintf(s->fp, "\n");
+	fprintf(s->fp, _("\n"));
 	strbuf_release(&onebuf);
 	strbuf_release(&twobuf);
 }
@@ -618,14 +619,14 @@ void wt_status_print(struct wt_status *s)
 	const char *branch_color = color(WT_STATUS_HEADER, s);
 
 	if (s->branch) {
-		const char *on_what = "On branch ";
+		const char *on_what = _("On branch ");
 		const char *branch_name = s->branch;
 		if (!prefixcmp(branch_name, "refs/heads/"))
 			branch_name += 11;
 		else if (!strcmp(branch_name, "HEAD")) {
 			branch_name = "";
 			branch_color = color(WT_STATUS_NOBRANCH, s);
-			on_what = "Not currently on any branch.";
+			on_what = _("Not currently on any branch.");
 		}
 		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "# ");
 		color_fprintf_ln(s->fp, branch_color, "%s%s", on_what, branch_name);
@@ -634,9 +635,9 @@ void wt_status_print(struct wt_status *s)
 	}
 
 	if (s->is_initial) {
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "# Initial commit");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("# Initial commit"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 	}
 
 	wt_status_print_updated(s);
@@ -647,38 +648,38 @@ void wt_status_print(struct wt_status *s)
 		wt_status_print_submodule_summary(s, 1);  /* unstaged */
 	}
 	if (s->show_untracked_files) {
-		wt_status_print_other(s, &s->untracked, "Untracked", "add");
+		wt_status_print_other(s, &s->untracked, _("Untracked"), _("add"));
 		if (s->show_ignored_files)
-			wt_status_print_other(s, &s->ignored, "Ignored", "add -f");
+			wt_status_print_other(s, &s->ignored, _("Ignored"), _("add -f"));
 	} else if (s->commitable)
-		fprintf(s->fp, "# Untracked files not listed%s\n",
+		fprintf(s->fp, _("# Untracked files not listed%s\n"),
 			advice_status_hints
-			? " (use -u option to show untracked files)" : "");
+			? _(" (use -u option to show untracked files)") : "");
 
 	if (s->verbose)
 		wt_status_print_verbose(s);
 	if (!s->commitable) {
 		if (s->amend)
-			fprintf(s->fp, "# No changes\n");
+			fprintf(s->fp, _("# No changes\n"));
 		else if (s->nowarn)
 			; /* nothing */
 		else if (s->workdir_dirty)
-			printf("no changes added to commit%s\n",
+			printf(_("no changes added to commit%s\n"),
 				advice_status_hints
-				? " (use \"git add\" and/or \"git commit -a\")" : "");
+				? _(" (use \"git add\" and/or \"git commit -a\")") : "");
 		else if (s->untracked.nr)
-			printf("nothing added to commit but untracked files present%s\n",
+			printf(_("nothing added to commit but untracked files present%s\n"),
 				advice_status_hints
-				? " (use \"git add\" to track)" : "");
+				? _(" (use \"git add\" to track)") : "");
 		else if (s->is_initial)
 			printf("nothing to commit%s\n", advice_status_hints
-				? " (create/copy files and use \"git add\" to track)" : "");
+				? _(" (create/copy files and use \"git add\" to track)") : "");
 		else if (!s->show_untracked_files)
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (use -u to show untracked files)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (use -u to show untracked files)") : "");
 		else
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (working directory clean)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (working directory clean)") : "");
 	}
 }
 
-- 
1.7.1.242.ge2b63.dirty
Ævar Arnfjörð Bjarmason· May 29, 2010, 22:45 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH 2/5] gitignore: Ignore files generated by gettext

  - Ignore po/git.pot which is autogenerated by a manual run of the
    Makefile. We're only interested in tracking the po/*.po files.
  - Ignore the /share/ directory. It's created during installation and
    contains gettext-related files to be installed.
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore    |    1 +
 po/.gitignore |    1 +
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 po/.gitignore
Show changes to 2 files +2 −0

.gitignore, po/.gitignore

diff --git a/.gitignore b/.gitignore
index 14e2b6b..d22c0e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -204,3 +204,4 @@
 *.pdb
 /Debug/
 /Release/
+/share/
diff --git a/po/.gitignore b/po/.gitignore
new file mode 100644
index 0000000..221000e
--- /dev/null
+++ b/po/.gitignore
@@ -0,0 +1 @@
+/*.pot
-- 
1.7.1.242.ge2b63.dirty
Ævar Arnfjörð Bjarmason· May 29, 2010, 22:45 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH 3/5] Makefile: Remove Gettext files on make clean

Remove the automatically generated po/git.pot and share/ files. They're created by the gettext installation process.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)
Show changes to Makefile +4 −0
diff --git a/Makefile b/Makefile
index 7989121..4de0627 100644
--- a/Makefile
+++ b/Makefile
@@ -2153,6 +2153,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
-- 
1.7.1.242.ge2b63.dirty
Ævar Arnfjörð Bjarmason· May 29, 2010, 22:45 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH 4/5] gettext: Add a skeleton po/is.po

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 po/is.po |  288 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 288 insertions(+), 0 deletions(-)
 create mode 100644 po/is.po
Show changes to po/is.po +288 −0
diff --git a/po/is.po b/po/is.po
new file mode 100644
index 0000000..4aac4e9
--- /dev/null
+++ b/po/is.po
@@ -0,0 +1,288 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Free Software Foundation, Inc.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+# SOME DESCRIPTIVE TITLE.#~ 
+# Copyright (C) YEAR Free Software Foundation, Inc.#~ 
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.#~ 
+##~ 
+#, fuzzy#~ 
+msgid ""#~ 
+msgstr ""#~ 
+"Project-Id-Version: Git\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-05-29 21:17+0000\n"
+"PO-Revision-Date: \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"
+
+#: wt-status.c:53
+msgid "# Unmerged paths:"
+msgstr ""
+
+#: wt-status.c:59 wt-status.c:76
+#, c-format
+msgid "#   (use \"git reset %s <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:61 wt-status.c:78
+msgid "#   (use \"git rm --cached <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:62
+msgid "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"
+msgstr ""
+
+#: wt-status.c:63 wt-status.c:79 wt-status.c:98 wt-status.c:110
+#: wt-status.c:115 wt-status.c:638 wt-status.c:640
+msgid "#"
+msgstr ""
+
+#: wt-status.c:70
+msgid "# Changes to be committed:"
+msgstr ""
+
+#: wt-status.c:88
+msgid "# Changed but not updated:"
+msgstr ""
+
+#: wt-status.c:92
+msgid "#   (use \"git add <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:94
+msgid "#   (use \"git add/rm <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:95
+msgid ""
+"#   (use \"git checkout -- <file>...\" to discard changes in working "
+"directory)"
+msgstr ""
+
+#: wt-status.c:97
+msgid "#   (commit or discard the untracked or modified content in submodules)"
+msgstr ""
+
+#: wt-status.c:106
+#, c-format
+msgid "# %s files:"
+msgstr ""
+
+#: wt-status.c:109
+#, c-format
+msgid "#   (use \"git %s <file>...\" to include in what will be committed)"
+msgstr ""
+
+#: wt-status.c:126
+msgid "bug"
+msgstr ""
+
+#: wt-status.c:129 wt-status.c:182
+msgid "#\t"
+msgstr ""
+
+#: wt-status.c:131
+msgid "both deleted:"
+msgstr ""
+
+#: wt-status.c:132
+msgid "added by us:"
+msgstr ""
+
+#: wt-status.c:133
+msgid "deleted by them:"
+msgstr ""
+
+#: wt-status.c:134
+msgid "added by them:"
+msgstr ""
+
+#: wt-status.c:135
+msgid "deleted by us:"
+msgstr ""
+
+#: wt-status.c:136
+msgid "both added:"
+msgstr ""
+
+#: wt-status.c:137
+msgid "both modified:"
+msgstr ""
+
+#: wt-status.c:139
+#, c-format
+msgid "%-20s%s\n"
+msgstr ""
+
+#: wt-status.c:165
+msgid " ("
+msgstr ""
+
+#: wt-status.c:167
+msgid "new commits, "
+msgstr ""
+
+#: wt-status.c:169
+msgid "modified content, "
+msgstr ""
+
+#: wt-status.c:171
+msgid "untracked content, "
+msgstr ""
+
+#: wt-status.c:185
+#, c-format
+msgid "new file:   %s"
+msgstr ""
+
+#: wt-status.c:188
+#, c-format
+msgid "copied:     %s -> %s"
+msgstr ""
+
+#: wt-status.c:191
+#, c-format
+msgid "deleted:    %s"
+msgstr ""
+
+#: wt-status.c:194
+#, c-format
+msgid "modified:   %s"
+msgstr ""
+
+#: wt-status.c:197
+#, c-format
+msgid "renamed:    %s -> %s"
+msgstr ""
+
+#: wt-status.c:200
+#, c-format
+msgid "typechange: %s"
+msgstr ""
+
+#: wt-status.c:203
+#, c-format
+msgid "unknown:    %s"
+msgstr ""
+
+#: wt-status.c:206
+#, c-format
+msgid "unmerged:   %s"
+msgstr ""
+
+#: wt-status.c:209
+#, c-format
+msgid "bug: unhandled diff status %c"
+msgstr ""
+
+#: wt-status.c:212
+#, c-format
+msgid "%s"
+msgstr ""
+
+#: wt-status.c:215
+#, c-format
+msgid "\n"
+msgstr ""
+
+#: wt-status.c:622
+msgid "On branch "
+msgstr "Á greininni "
+
+#: wt-status.c:629
+msgid "Not currently on any branch."
+msgstr "Ekki á neinni grein."
+
+#: wt-status.c:639
+msgid "# Initial commit"
+msgstr ""
+
+#: wt-status.c:651
+msgid "Untracked"
+msgstr ""
+
+#: wt-status.c:651
+msgid "add"
+msgstr ""
+
+#: wt-status.c:653
+msgid "Ignored"
+msgstr ""
+
+#: wt-status.c:653
+msgid "add -f"
+msgstr ""
+
+#: wt-status.c:655
+#, c-format
+msgid "# Untracked files not listed%s\n"
+msgstr ""
+
+#: wt-status.c:657
+msgid " (use -u option to show untracked files)"
+msgstr ""
+
+#: wt-status.c:663
+#, c-format
+msgid "# No changes\n"
+msgstr "# Engar breytingar\n"
+
+#: wt-status.c:667
+#, c-format
+msgid "no changes added to commit%s\n"
+msgstr ""
+
+#: wt-status.c:669
+msgid " (use \"git add\" and/or \"git commit -a\")"
+msgstr ""
+
+#: wt-status.c:671
+#, c-format
+msgid "nothing added to commit but untracked files present%s\n"
+msgstr ""
+
+#: wt-status.c:673
+msgid " (use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:676
+msgid " (create/copy files and use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:678 wt-status.c:681
+#, c-format
+msgid "nothing to commit%s\n"
+msgstr ""
+
+#: wt-status.c:679
+msgid " (use -u to show untracked files)"
+msgstr ""
+
+#: wt-status.c:682
+msgid " (working directory clean)"
+msgstr ""
+
+#: git-pull.sh:124
+msgid "Fetching tags only, you probably meant:"
+msgstr ""
+
+#: git-pull.sh:125
+msgid "  git fetch --tags"
+msgstr ""
-- 
1.7.1.242.ge2b63.dirty
Ævar Arnfjörð Bjarmason· May 29, 2010, 22:45 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH 5/5] Add infrastructure to make shellscripts translatable

Use GNU's gettext.sh in git-sh-setup if it's available, otherwise fallback on our own custom functions.

A couple of strings in git-pull.sh are now translatable as a proof of concept, including a multiline string.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile        |    7 +++++--
 git-pull.sh     |   15 ++++++++-------
 git-sh-setup.sh |   33 +++++++++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 9 deletions(-)
Show changes to 3 files +46 −9

Makefile, git-pull.sh, git-sh-setup.sh

diff --git a/Makefile b/Makefile
index 4de0627..dce2faa 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 localedir sysconfdir
 
 CC = gcc
 AR = ar
@@ -1455,6 +1456,7 @@ 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))
@@ -1548,6 +1550,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 >$@+
@@ -1881,7 +1884,7 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-pull.sh b/git-pull.sh
index 1a4729f..22a6da2 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -121,8 +121,8 @@ error_on_no_merge_candidates () {
 	do
 		case "$opt" in
 		-t|--t|--ta|--tag|--tags)
-			echo "Fetching tags only, you probably meant:"
-			echo "  git fetch --tags"
+			gettext "Fetching tags only, you probably meant:"; echo
+			gettext "  git fetch --tags"; echo
 			exit 1
 		esac
 	done
@@ -154,11 +154,12 @@ error_on_no_merge_candidates () {
 		echo "a branch. Because this is not the default configured remote"
 		echo "for your current branch, you must specify a branch on the command line."
 	elif [ -z "$curr_branch" ]; then
-		echo "You are not currently on a branch, so I cannot use any"
-		echo "'branch.<branchname>.merge' in your configuration file."
-		echo "Please specify which remote branch you want to use on the command"
-		echo "line and try again (e.g. 'git pull <repository> <refspec>')."
-		echo "See git-pull(1) for details."
+        gettext "You are not currently on a branch, so I cannot use any
+'branch.<branchname>.merge' in your configuration file.
+Please specify which remote branch you want to use on the command
+line and try again (e.g. 'git pull <repository> <refspec>').
+See git-pull(1) for details.";
+        echo
 	elif [ -z "$upstream" ]; then
 		echo "You asked me to pull without telling me which branch you"
 		echo "want to $op_type $op_prep, and 'branch.${curr_branch}.merge' in"
diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 6131670..c8010f2 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -211,3 +211,36 @@ case $(uname -s) in
 	}
 	;;
 esac
+
+# Try to use libintl's gettext.sh, or fall back to English if we
+# can't.
+. gettext.sh
+if test $? -ne 0
+then
+    TEXTDOMAIN=git
+    export TEXDTOMAIN
+    TEXTDOMAINDIR="@@LOCALEDIR@@"
+    export TEXTDOMAINDIR
+else
+    # Since GNU gettext.sh isn't available we'll have to define our
+    # own dummy functions.
+
+    # This code adapted from NessusClient-1.0.2's nessusclient-mkcert
+    # by Michel Arboi <arboi@alussinan.org>. The original code is
+    # under the GPLv2.
+
+    # Not everyone has echo -n
+    case $(echo -n) in
+        \-n)    Xn=   ; Xc='\c' ;;
+        *)      Xn=-n ; Xc=
+    esac
+
+    gettext () {
+        echo $Xn "$1" $Xc
+    }
+
+    eval_gettext () {
+        eval_gettext_var="echo $1"
+        echo $Xn `eval $eval_gettext_var` $Xc
+    }
+fi
-- 
1.7.1.242.ge2b63.dirty
Jonathan Nieder· May 30, 2010, 01:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC 0/5] Add internationalization support to Git

Hi Ævar,
Ævar Arnfjörð Bjarmason wrote:
Show 6 quoted lines
>     I made three strings in git-pull.sh translatable as a proof of
>     concept. One problem that I ran into is that xgettext(1) seems
>     very particular when picking up translation strings. It accepts
>     this:
> 
>         gettext "hello world"; echo
Does ‘gettext -s "hello world"’ work, too?  (Just curious.)
>     but not this:
[...]
Show 7 quoted lines
> 
>         gettext <<"END";
> hello world
> END
> 
>     Maybe there's a way to make it play nice. But I just used a large
>     multiline string as a workaround.

Not so nice, but it seems that gettext expects a message id as an argument (i.e., it will only replace echo and not cat).

>     I don't know what to do about
>     'die gettext' other than define a 'die_gettext' wrapper function
>     and use `xgettext --keyword=die_gettext'.
Sounds sensible.
> One thing I haven't done is to try to go ahead and make massive
> changes to the Git source code to make everything translatable.
I am vaguely worried about performance.  Suppose a function does
	for (i = 0; i < 1000000; i++)
		printf(_("Some interesting label: %s\n"), foo(i));
Will this compile to the equivalent of
	const char *s = _("Some interesting label: %s\n");
	for (i = 0; i < 1000000; i++)
		printf(s, foo(i));

Suppose someone decides to make that change by hand (maybe the loop is too large for the compiler to notice the potential winnings). Then presumably gcc cannot be able to type-check the format any more. Is there some way around this that avoids both speed regressions and loss of type-safety?

Apologies if this was already answered in the earlier discussion.

Thanks, Jonathan

Ævar Arnfjörð Bjarmason· May 30, 2010, 16:04 UTC · re: Jonathan Nieder · lore

Re: [PATCH/RFC 0/5] Add internationalization support to Git

On Sun, May 30, 2010 at 01:46, Jonathan Nieder <jrnieder@gmail.com> wrote:
> Hi Ævar,
Hi, and thanks for taking the time to review this.
Show 10 quoted lines
> Ævar Arnfjörð Bjarmason wrote:
>
>>     I made three strings in git-pull.sh translatable as a proof of
>>     concept. One problem that I ran into is that xgettext(1) seems
>>     very particular when picking up translation strings. It accepts
>>     this:
>>
>>         gettext "hello world"; echo
>
> Does ‘gettext -s "hello world"’ work, too?  (Just curious.)

No, that just makes "-s" translatable. Even options that gettext accepts don't work either, you have to use eval_gettext "\$foo" instead of gettext -e "\$foo". The xgettext program is quite naïve like that.

Show 12 quoted lines
>>     but not this:
> [...]
>>
>>         gettext <<"END";
>> hello world
>> END
>>
>>     Maybe there's a way to make it play nice. But I just used a large
>>     multiline string as a workaround.
>
> Not so nice, but it seems that gettext expects a message id as
> an argument (i.e., it will only replace echo and not cat).

Yes. I mailed the maintainer about this. gettext would need to accept text on STDIN and xgettext would need to find the messages for it to work.

In the meantime we could just use multiline strings. It works for the test suite.

Show 25 quoted lines
>>     I don't know what to do about
>>     'die gettext' other than define a 'die_gettext' wrapper function
>>     and use `xgettext --keyword=die_gettext'.
>
> Sounds sensible.
>
>> One thing I haven't done is to try to go ahead and make massive
>> changes to the Git source code to make everything translatable.
>
> I am vaguely worried about performance.  Suppose a function does
>
>        for (i = 0; i < 1000000; i++)
>                printf(_("Some interesting label: %s\n"), foo(i));
>
> Will this compile to the equivalent of
>
>        const char *s = _("Some interesting label: %s\n");
>        for (i = 0; i < 1000000; i++)
>                printf(s, foo(i));
>
> Suppose someone decides to make that change by hand (maybe the
> loop is too large for the compiler to notice the potential
> winnings).  Then presumably gcc cannot be able to type-check the
> format any more.  Is there some way around this that avoids
> both speed regressions and loss of type-safety?

Any level of indirection is of course going to be slower, there's no way around that. I made two test programs to test this out:

test-in-loop.c:
    #include <stdio.h>
    #include <stdlib.h>
    #include <locale.h>
    #include <libintl.h>
    #define _(s) gettext(s)
    int foo(long int x) {
        return x * x;
    }
    int main(void) {
        const char *podir = "/usr/local/share/locale";
        if(!podir) puts("zomg error");
        char *ret = bindtextdomain("git", podir);
        ret = setlocale(LC_MESSAGES, "");
        ret = setlocale(LC_CTYPE, "");
        ret = textdomain("git");
        for (long int i = 0; i < 10000000; i++) {
            printf(_("Some interesting label: %ld\n"), foo(i));
        }
        return 0;
    }
test-outside-loop.c:
    #include <stdio.h>
    #include <stdlib.h>
    #include <locale.h>
    #include <libintl.h>
    #define _(s) gettext(s)
    int foo(long int x) {
        return x * x;
    }
    int main(void) {
        const char *podir = "/usr/local/share/locale";
        if(!podir) puts("zomg error");
        char *ret = bindtextdomain("git", podir);
        ret = setlocale(LC_MESSAGES, "");
        ret = setlocale(LC_CTYPE, "");
        ret = textdomain("git");
        const char *s = _("Some interesting label: %ld\n");
        for (long int i = 0; i < 10000000; i++)
            printf(s, foo(i));
        return 0;
    }

Note that I use 10 million iterations, not 1 million like in your example.

Here's how they compile:
    $ gcc -std=c99 -o test-in-loop test-in-loop.c ; gcc -std=c99 -o
test-outside-loop test-outside-loop.c
    test-in-loop.c: In function ‘main’:
    test-in-loop.c:21: warning: format ‘%ld’ expects type ‘long int’,
but argument 2 has type ‘int’

I.e. your concerns are valid. GCC won't catch an invalid format specifier in this case.

And even though gettext tries to make cases like these fast (http://www.gnu.org/software/hello/manual/gettext/Optimized-gettext.html) it's still a lot slower than hardcoded English:

    perl -MBenchmark=:all -MData::Dump=dump -E 'cmpthese(10, {
         outside => sub { system "./test-outside-loop >/dev/null" },
         inside =>  sub { system "./test-in-loop >/dev/null" },
    });'
            s/iter  inside outside
    inside    13.4      --    -83%
    outside   2.26    495%      --
> Apologies if this was already answered in the earlier discussion.

What you can do (and this was covered) is to use msgfmt to check that no translations use different format specifiers. But hopefully cases where you have messages like these in tight loops and the message lookup itself is a significant contributor to the program time will be so rare as to not be an issue.

Jonathan Nieder· May 30, 2010, 22:23 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC 0/5] Add internationalization support to Git

Ævar Arnfjörð Bjarmason wrote:
Show 12 quoted lines
> And even though gettext tries to make cases like these fast
> (http://www.gnu.org/software/hello/manual/gettext/Optimized-gettext.html)
> it's still a lot slower than hardcoded English:
> 
>     perl -MBenchmark=:all -MData::Dump=dump -E 'cmpthese(10, {
>          outside => sub { system "./test-outside-loop >/dev/null" },
>          inside =>  sub { system "./test-in-loop >/dev/null" },
>     });'
> 
>             s/iter  inside outside
>     inside    13.4      --    -83%
>     outside   2.26    495%      --
Given:

-- 8< -- #include <stdio.h> #include <stdlib.h> #include <locale.h> #include <libintl.h> #include "gettext.h"

int foo(long int x) {
	return x * x;
}
int main(void) {
	const char *podir = "/usr/local/share/locale";
	long int i;
	bindtextdomain("git", podir);
	setlocale(LC_MESSAGES, "");
	setlocale(LC_CTYPE, "");
	textdomain("git");
	for (i = 0; i < 1000000; i++)
		printf(_("Some interesting label: %ld\n"), foo(i));
	return 0;
}
-- >8 --

No message catalog is installed here, and I compile with gcc-4.5 -Wall -W -O2. The results are similar.

A: the standard way.  gettext.h contains "#define _(s) gettext(s)" or
| static inline char *_(const char *s) __attribute__((__format_arg(1)__))
| {
|	return gettext(s);
| }
 6.74user 0.02system 0:06.78elapsed 99%CPU (0avgtext+0avgdata 2304maxresident)k
 0inputs+0outputs (0major+182minor)pagefaults 0swaps
 (about 7 seconds.)
B: noop.  gettext.h contains "#define _(s) s"
 1.35user 0.01system 0:01.37elapsed 99%CPU (0avgtext+0avgdata 2192maxresident)k
 0inputs+0outputs (0major+172minor)pagefaults 0swaps
 (about 1.5 seconds.)

It would seem that __attribute__((__pure__)) should let the compiler give us the best of both worlds, but no luck. Even __attribute__((__const__)) is ignored; the compiler inlines the body of _() before it has a chance to notice.

We can fool the compiler into paying attention by making it not inlinable: if gettext.h contains

| extern char *_(const char *s) __attribute__((__format_arg__(1), __const__));
and a separate gettext.c contains
| #include <libintl.h>
| #include "gettext.h"
| char *_(const char *s) { return gettext(s); }
we get the performance of B again:
 1.36user 0.01system 0:01.38elapsed 98%CPU (0avgtext+0avgdata 2304maxresident)k
 0inputs+0outputs (0major+180minor)pagefaults 0swaps

This amounts to lying to the compiler, since it is possible for the string pointed to by a single address s to differ between calls to _. The __pure__ attribute would be more honest, but for reasons I don’t understand it suppresses the optimization.

Moral of the story: at least in simple cases, we can keep the performance and the typechecking. Phew.

HTH, Jonathan

Ævar Arnfjörð Bjarmason· May 31, 2010, 12:17 UTC · re: Jonathan Nieder · lore

Re: [PATCH/RFC 0/5] Add internationalization support to Git

On Sun, May 30, 2010 at 22:23, Jonathan Nieder <jrnieder@gmail.com> wrote:
Show 29 quoted lines
> [...]
> It would seem that __attribute__((__pure__)) should let the compiler give
> us the best of both worlds, but no luck.  Even __attribute__((__const__))
> is ignored; the compiler inlines the body of _() before it has a chance
> to notice.
>
> We can fool the compiler into paying attention by making it not
> inlinable: if gettext.h contains
>
> | extern char *_(const char *s) __attribute__((__format_arg__(1), __const__));
>
> and a separate gettext.c contains
>
> | #include <libintl.h>
> | #include "gettext.h"
> | char *_(const char *s) { return gettext(s); }
>
> we get the performance of B again:
>
>  1.36user 0.01system 0:01.38elapsed 98%CPU (0avgtext+0avgdata 2304maxresident)k
>  0inputs+0outputs (0major+180minor)pagefaults 0swaps
>
> This amounts to lying to the compiler, since it is possible for the string
> pointed to by a single address s to differ between calls to _.  The __pure__
> attribute would be more honest, but for reasons I don’t understand it
> suppresses the optimization.
>
> Moral of the story: at least in simple cases, we can keep the performance
> and the typechecking.  Phew.

Awesome, how portable is this? If it's just GCC I guess it can just be wrapped in #ifdef __GNUC__.

Thanks for investingating this.
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 0/6] Add internationalization support to Git

Here's version 2 of the patch series begun by Jeff Epler to make Git optionally translatable with GNU gettext.

Due to an error of mine only the cover letter of the previous series had "PATCH/RFC" in the subject line. That's rectified in this series.

Aside from making sure the test suite always runs under LC_ALL=C, sanity tests for the translations, "how to translate" documentation, and the Perl Makefile issue noted below, this series is pretty much ready for inclusion in my opinion. Perl support was the last missing piece for translating all of the core C+Shell+Perl code.

New stuff:
  * Perl scripts are now translatable through the Gettext
    facility. Like the shellscript facility I added this has a
    graceful fallback if the prerequisite libraries aren't installed.
    I'm using the libintl-perl library which provides
    Locale::Messages. I could also have used liblocale-gettext-perl
    which provides Locale::gettext. Debian seems to prefer the latter,
    but the former is recomended by GNU. The new Git::Gettext library
    could probably be modified to try both.
    It works 100% for me. Aside from this bit:
    
    	# TODO: How do I make the sed replacements in the top level
    	# Makefile reach me here?
    	#our $TEXTDOMAINDIR = q|@@LOCALEDIR@@|;
    	our $TEXTDOMAINDIR = q</usr/local/share/locale>;
    I couldn't figure out how to make the sed substitution the core
    /Makefile does for e.g. @@GIT_VERSION@@ work for the hybrid
    Makefile / perl.mak setup in perl/.
    Suggestions from contributors more familiar with Git's build
    system welcome.
  * Another thing to note regarding Perl. We have some Perl code
    that's obviously intended to work with perls as old as 5.6. It
    would be preferable to translation support if the minimum were
    bumped to 5.8. Here's why:
        $ perl -e 'printf "%2\$s the %1\$s has\n", ("Clone War", "Begun")'
        Begun the Clone War has
    Perl 5.6's printf doesn't support that printf syntax, which means
    that translators would have to make do with the word order of the
    English messages.
    I haven't seen any documentation about Perl coding guidelines or
    Perl versions that we aim to support in Documentation/, so I don't
    know if 5.6 is supposed to be supported or whether the support is
    just incidental.
    Note that this 5.8 could only be a requirement if the user was
    using a translation with positional printf formats. Users
    interested in running the English-only version could still do so
    on 5.6.
  * Updated is.po. It now contains example translations for C, Shell
    and Perl programs as a proof of concept.
  * Rewrote the commit message of the shell script support commit to
    use use the gettext: prefix.
    
Jeff Epler (1):
  Add infrastructure for translating Git with gettext
Ævar Arnfjörð Bjarmason (5):
  gitignore: Ignore files generated by gettext
  Makefile: Remove Gettext files on make clean
  gettext: Add a Gettext interface for shell scripts
  gettext: Add a Gettext interface for Perl
  gettext: Add a skeleton po/is.po
 .gitignore          |    1 +
 Makefile            |   37 +++++++-
 gettext.c           |   17 +++
 gettext.h           |   15 +++
 git-pull.sh         |   15 ++--
 git-send-email.perl |    3 +-
 git-sh-setup.sh     |   33 ++++++
 git.c               |    3 +
 perl/Git/Gettext.pm |   83 +++++++++++++++
 perl/Makefile.PL    |    5 +-
 po/.gitignore       |    1 +
 po/is.po            |  282 +++++++++++++++++++++++++++++++++++++++++++++++++++
 wt-status.c         |  129 ++++++++++++------------
 13 files changed, 550 insertions(+), 74 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 create mode 100644 perl/Git/Gettext.pm
 create mode 100644 po/.gitignore
 create mode 100644 po/is.po
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 1/6] Add infrastructure for translating Git with gettext

From: Jeff Epler <jepler@unpythonic.net>

Change the build process to use GNU's libintl if it's available. If not we define our own skeleton replacement functions which degrade gracefully to English.

Signed-off-by: Jeff Epler <jepler@unpythonic.net>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile    |   26 ++++++++++++
 gettext.c   |   17 ++++++++
 gettext.h   |   15 +++++++
 git.c       |    3 +
 wt-status.c |  129 ++++++++++++++++++++++++++++++-----------------------------
 5 files changed, 126 insertions(+), 64 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
Show changes to 5 files +126 −64

Makefile, gettext.c, gettext.h, git.c, wt-status.c

diff --git a/Makefile b/Makefile
index d5d6565..7989121 100644
--- a/Makefile
+++ b/Makefile
@@ -297,6 +297,8 @@ RPMBUILD = rpmbuild
 TCL_PATH = tclsh
 TCLTK_PATH = wish
 PTHREAD_LIBS = -lpthread
+XGETTEXT = xgettext
+MSGFMT = msgfmt
 
 export TCL_PATH TCLTK_PATH
 
@@ -523,6 +525,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 +567,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 +1390,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 +1425,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,6 +1453,7 @@ gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
 template_dir_SQ = $(subst ','\'',$(template_dir))
 htmldir_SQ = $(subst ','\'',$(htmldir))
 prefix_SQ = $(subst ','\'',$(prefix))
+sharedir_SQ = $(subst ','\'',$(sharedir))
 
 SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
@@ -1868,6 +1880,17 @@ cscope:
 	$(RM) cscope*
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
+pot:
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+
+POFILES := $(wildcard po/*.po)
+MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
+MODIRS := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/,$(POFILES))
+all:: $(MOFILES)
+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 +2003,9 @@ 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)'
+	$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(sharedir_SQ)/locale'
+	(cd share && tar cf - locale) | \
+		(cd '$(DESTDIR_SQ)$(sharedir_SQ)' && umask 022 && tar xof -)
 	$(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install
 ifndef NO_PERL
 	$(MAKE) -C perl prefix='$(prefix_SQ)' DESTDIR='$(DESTDIR_SQ)' install
diff --git a/gettext.c b/gettext.c
new file mode 100644
index 0000000..aadce19
--- /dev/null
+++ b/gettext.c
@@ -0,0 +1,17 @@
+#ifdef NO_GETTEXT
+void git_setup_gettext() {}
+#else
+#include "exec_cmd.h"
+#include <libintl.h>
+#include <stdlib.h>
+
+void git_setup_gettext() {
+    const char *podir = system_path("share/locale");
+    if(!podir) return;
+    char *ret = bindtextdomain("git", podir);
+    free((void*)podir);
+    ret = setlocale(LC_MESSAGES, "");
+    ret = setlocale(LC_CTYPE, "");
+    ret = textdomain("git");
+}
+#endif
diff --git a/gettext.h b/gettext.h
new file mode 100644
index 0000000..8b221b4
--- /dev/null
+++ b/gettext.h
@@ -0,0 +1,15 @@
+#ifndef GETTEXT_H
+#define GETTEXT_H
+
+void git_setup_gettext();
+
+#ifdef NO_GETTEXT
+#define _(s) (s)
+#define N_(s) (s)
+#else
+#include <libintl.h>
+#define _(s) gettext(s)
+#define N_(s) (s)
+#endif
+
+#endif
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/wt-status.c b/wt-status.c
index 14e0acc..70b4293 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -9,6 +9,7 @@
 #include "quote.h"
 #include "run-command.h"
 #include "remote.h"
+#include "gettext.h"
 
 static char default_wt_status_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_NORMAL, /* WT_STATUS_HEADER */
@@ -49,33 +50,33 @@ static void wt_status_print_unmerged_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Unmerged paths:");
+	color_fprintf_ln(s->fp, c, _("# Unmerged paths:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		;
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_cached_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changes to be committed:");
+	color_fprintf_ln(s->fp, c, _("# Changes to be committed:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		; /* NEEDSWORK: use "git reset --unresolve"??? */
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_dirty_header(struct wt_status *s,
@@ -84,17 +85,17 @@ static void wt_status_print_dirty_header(struct wt_status *s,
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changed but not updated:");
+	color_fprintf_ln(s->fp, c, _("# Changed but not updated:"));
 	if (!advice_status_hints)
 		return;
 	if (!has_deleted)
-		color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to update what will be committed)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add <file>...\" to update what will be committed)"));
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" to update what will be committed)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git checkout -- <file>...\" to discard changes in working directory)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" to update what will be committed)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git checkout -- <file>...\" to discard changes in working directory)"));
 	if (has_dirty_submodules)
-		color_fprintf_ln(s->fp, c, "#   (commit or discard the untracked or modified content in submodules)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (commit or discard the untracked or modified content in submodules)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_other_header(struct wt_status *s,
@@ -102,16 +103,16 @@ static void wt_status_print_other_header(struct wt_status *s,
 					 const char *how)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
-	color_fprintf_ln(s->fp, c, "# %s files:", what);
+	color_fprintf_ln(s->fp, c, _("# %s files:"), what);
 	if (!advice_status_hints)
 		return;
-	color_fprintf_ln(s->fp, c, "#   (use \"git %s <file>...\" to include in what will be committed)", how);
-	color_fprintf_ln(s->fp, c, "#");
+	color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to include in what will be committed)"), how);
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_trailer(struct wt_status *s)
 {
-	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 }
 
 #define quote_path quote_path_relative
@@ -122,20 +123,20 @@ static void wt_status_print_unmerged_data(struct wt_status *s,
 	const char *c = color(WT_STATUS_UNMERGED, s);
 	struct wt_status_change_data *d = it->util;
 	struct strbuf onebuf = STRBUF_INIT;
-	const char *one, *how = "bug";
+	const char *one, *how = _("bug");
 
 	one = quote_path(it->string, -1, &onebuf, s->prefix);
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (d->stagemask) {
-	case 1: how = "both deleted:"; break;
-	case 2: how = "added by us:"; break;
-	case 3: how = "deleted by them:"; break;
-	case 4: how = "added by them:"; break;
-	case 5: how = "deleted by us:"; break;
-	case 6: how = "both added:"; break;
-	case 7: how = "both modified:"; break;
+	case 1: how = _("both deleted:"); break;
+	case 2: how = _("added by us:"); break;
+	case 3: how = _("deleted by them:"); break;
+	case 4: how = _("added by them:"); break;
+	case 5: how = _("deleted by us:"); break;
+	case 6: how = _("both added:"); break;
+	case 7: how = _("both modified:"); break;
 	}
-	color_fprintf(s->fp, c, "%-20s%s\n", how, one);
+	color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);
 	strbuf_release(&onebuf);
 }
 
@@ -161,13 +162,13 @@ static void wt_status_print_change_data(struct wt_status *s,
 		break;
 	case WT_STATUS_CHANGED:
 		if (d->new_submodule_commits || d->dirty_submodule) {
-			strbuf_addstr(&extra, " (");
+			strbuf_addstr(&extra, _(" ("));
 			if (d->new_submodule_commits)
-				strbuf_addf(&extra, "new commits, ");
+				strbuf_addf(&extra, _("new commits, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
-				strbuf_addf(&extra, "modified content, ");
+				strbuf_addf(&extra, _("modified content, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_UNTRACKED)
-				strbuf_addf(&extra, "untracked content, ");
+				strbuf_addf(&extra, _("untracked content, "));
 			strbuf_setlen(&extra, extra.len - 2);
 			strbuf_addch(&extra, ')');
 		}
@@ -178,40 +179,40 @@ static void wt_status_print_change_data(struct wt_status *s,
 	one = quote_path(one_name, -1, &onebuf, s->prefix);
 	two = quote_path(two_name, -1, &twobuf, s->prefix);
 
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (status) {
 	case DIFF_STATUS_ADDED:
-		color_fprintf(s->fp, c, "new file:   %s", one);
+		color_fprintf(s->fp, c, _("new file:   %s"), one);
 		break;
 	case DIFF_STATUS_COPIED:
-		color_fprintf(s->fp, c, "copied:     %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("copied:     %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_DELETED:
-		color_fprintf(s->fp, c, "deleted:    %s", one);
+		color_fprintf(s->fp, c, _("deleted:    %s"), one);
 		break;
 	case DIFF_STATUS_MODIFIED:
-		color_fprintf(s->fp, c, "modified:   %s", one);
+		color_fprintf(s->fp, c, _("modified:   %s"), one);
 		break;
 	case DIFF_STATUS_RENAMED:
-		color_fprintf(s->fp, c, "renamed:    %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("renamed:    %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_TYPE_CHANGED:
-		color_fprintf(s->fp, c, "typechange: %s", one);
+		color_fprintf(s->fp, c, _("typechange: %s"), one);
 		break;
 	case DIFF_STATUS_UNKNOWN:
-		color_fprintf(s->fp, c, "unknown:    %s", one);
+		color_fprintf(s->fp, c, _("unknown:    %s"), one);
 		break;
 	case DIFF_STATUS_UNMERGED:
-		color_fprintf(s->fp, c, "unmerged:   %s", one);
+		color_fprintf(s->fp, c, _("unmerged:   %s"), one);
 		break;
 	default:
-		die("bug: unhandled diff status %c", status);
+		die(_("bug: unhandled diff status %c"), status);
 	}
 	if (extra.len) {
-		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s", extra.buf);
+		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("%s"), extra.buf);
 		strbuf_release(&extra);
 	}
-	fprintf(s->fp, "\n");
+	fprintf(s->fp, _("\n"));
 	strbuf_release(&onebuf);
 	strbuf_release(&twobuf);
 }
@@ -618,14 +619,14 @@ void wt_status_print(struct wt_status *s)
 	const char *branch_color = color(WT_STATUS_HEADER, s);
 
 	if (s->branch) {
-		const char *on_what = "On branch ";
+		const char *on_what = _("On branch ");
 		const char *branch_name = s->branch;
 		if (!prefixcmp(branch_name, "refs/heads/"))
 			branch_name += 11;
 		else if (!strcmp(branch_name, "HEAD")) {
 			branch_name = "";
 			branch_color = color(WT_STATUS_NOBRANCH, s);
-			on_what = "Not currently on any branch.";
+			on_what = _("Not currently on any branch.");
 		}
 		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "# ");
 		color_fprintf_ln(s->fp, branch_color, "%s%s", on_what, branch_name);
@@ -634,9 +635,9 @@ void wt_status_print(struct wt_status *s)
 	}
 
 	if (s->is_initial) {
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "# Initial commit");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("# Initial commit"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 	}
 
 	wt_status_print_updated(s);
@@ -647,38 +648,38 @@ void wt_status_print(struct wt_status *s)
 		wt_status_print_submodule_summary(s, 1);  /* unstaged */
 	}
 	if (s->show_untracked_files) {
-		wt_status_print_other(s, &s->untracked, "Untracked", "add");
+		wt_status_print_other(s, &s->untracked, _("Untracked"), _("add"));
 		if (s->show_ignored_files)
-			wt_status_print_other(s, &s->ignored, "Ignored", "add -f");
+			wt_status_print_other(s, &s->ignored, _("Ignored"), _("add -f"));
 	} else if (s->commitable)
-		fprintf(s->fp, "# Untracked files not listed%s\n",
+		fprintf(s->fp, _("# Untracked files not listed%s\n"),
 			advice_status_hints
-			? " (use -u option to show untracked files)" : "");
+			? _(" (use -u option to show untracked files)") : "");
 
 	if (s->verbose)
 		wt_status_print_verbose(s);
 	if (!s->commitable) {
 		if (s->amend)
-			fprintf(s->fp, "# No changes\n");
+			fprintf(s->fp, _("# No changes\n"));
 		else if (s->nowarn)
 			; /* nothing */
 		else if (s->workdir_dirty)
-			printf("no changes added to commit%s\n",
+			printf(_("no changes added to commit%s\n"),
 				advice_status_hints
-				? " (use \"git add\" and/or \"git commit -a\")" : "");
+				? _(" (use \"git add\" and/or \"git commit -a\")") : "");
 		else if (s->untracked.nr)
-			printf("nothing added to commit but untracked files present%s\n",
+			printf(_("nothing added to commit but untracked files present%s\n"),
 				advice_status_hints
-				? " (use \"git add\" to track)" : "");
+				? _(" (use \"git add\" to track)") : "");
 		else if (s->is_initial)
 			printf("nothing to commit%s\n", advice_status_hints
-				? " (create/copy files and use \"git add\" to track)" : "");
+				? _(" (create/copy files and use \"git add\" to track)") : "");
 		else if (!s->show_untracked_files)
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (use -u to show untracked files)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (use -u to show untracked files)") : "");
 		else
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (working directory clean)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (working directory clean)") : "");
 	}
 }
 
-- 
1.7.1.248.gcd6d1
Jakub Narebski· Jun 1, 2010, 17:01 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v2 1/6] Add infrastructure for translating Git with gettext

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
Show 5 quoted lines
> From: Jeff Epler <jepler@unpythonic.net>
> 
> Change the build process to use GNU's libintl if it's available. If
> not we define our own skeleton replacement functions which degrade
> gracefully to English.
Very nice.
Show 12 quoted lines
> 
> Signed-off-by: Jeff Epler <jepler@unpythonic.net>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  Makefile    |   26 ++++++++++++
>  gettext.c   |   17 ++++++++
>  gettext.h   |   15 +++++++
>  git.c       |    3 +
>  wt-status.c |  129 ++++++++++++++++++++++++++++++-----------------------------
>  5 files changed, 126 insertions(+), 64 deletions(-)
>  create mode 100644 gettext.c
>  create mode 100644 gettext.h

Could you also provide change to configure.ac, so that ./configure would detect if we have gettext installed or not? Thanks in advance.

-- 
Jakub Narebski
Poland
ShadeHawk on #git
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 18:11 UTC · re: Jakub Narebski · lore

[PATCH/RCF] autoconf: Check if <libintl.h> exists and set NO_GETTEXT

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
On Tue, Jun 1, 2010 at 17:01, Jakub Narebski <jnareb@gmail.com> wrote:
> [...]
> Could you also provide change to configure.ac, so that ./configure
> would detect if we have gettext installed or not?  Thanks in advance.

Here's a check that just checks if we have libintl.h similar to the existing checks for libgen.h and other headers.

Do you think this be adequate?
 config.mak.in |    1 +
 configure.ac  |    6 ++++++
 2 files changed, 7 insertions(+), 0 deletions(-)
Show changes to 2 files +7 −0

config.mak.in, configure.ac

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=],
-- 
1.7.1.243.g01eae
Jakub Narebski· Jun 1, 2010, 21:22 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RCF] autoconf: Check if <libintl.h> exists and set NO_GETTEXT

On Tue, Jun 1, 2010, Ævar Arnfjörð Bjarmason wrote:
Show 9 quoted lines
> On Tue, Jun 1, 2010 at 17:01, Jakub Narebski <jnareb@gmail.com> wrote:
> > [...]
> > Could you also provide change to configure.ac, so that ./configure
> > would detect if we have gettext installed or not?  Thanks in advance.
> 
> Here's a check that just checks if we have libintl.h similar to the
> existing checks for libgen.h and other headers.
> 
> Do you think this be adequate?

I'm not sure. If NO_GETTEXT is used only to protect '#include <libintl.h>' it might be enough.

Show 17 quoted lines
> 
>  config.mak.in |    1 +
>  configure.ac  |    6 ++++++
>  2 files changed, 7 insertions(+), 0 deletions(-)
> 
> 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@
O.K.
Show 17 quoted lines
> 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=],

The getext documentation says that `gettext.m4' defines AM_GNU_GETTEXT macro that tests for the presence of GNU gettext function family.

The complication is that `gettext.m4' might not exist, and then AM_GNU_GETTEXT would be not defined. So if we are to use AM_GNU_GETTEXT, we should I guess protect it with m4_ifdef():

  m4_ifdef([AM_GNU_GETTEXT], [AM_GNU_GETTEXT([external])])

or something like that. And of course we need to work around the fact that AM_GNU_GETTEXT sets USE_NLS to either 'yes' or 'no' instead of setting setting NO_GETTEXT to either non-empty or empty value.

Or we can do something similar to what we do for NEEDS_LIBICINV / NO_ICONV, namely make ./configure compile simple test program using gettext.

Cc-ed Ben Walton and David M. Syzdek: perhaps they can help with this issue.

-- 
Jakub Narebski
Poland
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 2/6] gitignore: Ignore files generated by gettext

  - Ignore po/git.pot which is autogenerated by a manual run of the
    Makefile. We're only interested in tracking the po/*.po files.
  - Ignore the /share/ directory. It's created during installation and
    contains gettext-related files to be installed.
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore    |    1 +
 po/.gitignore |    1 +
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 po/.gitignore
Show changes to 2 files +2 −0

.gitignore, po/.gitignore

diff --git a/.gitignore b/.gitignore
index 14e2b6b..d22c0e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -204,3 +204,4 @@
 *.pdb
 /Debug/
 /Release/
+/share/
diff --git a/po/.gitignore b/po/.gitignore
new file mode 100644
index 0000000..221000e
--- /dev/null
+++ b/po/.gitignore
@@ -0,0 +1 @@
+/*.pot
-- 
1.7.1.248.gcd6d1
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 3/6] Makefile: Remove Gettext files on make clean

Remove the automatically generated po/git.pot and share/ files. They're created by the gettext installation process.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)
Show changes to Makefile +4 −0
diff --git a/Makefile b/Makefile
index 7989121..4de0627 100644
--- a/Makefile
+++ b/Makefile
@@ -2153,6 +2153,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
-- 
1.7.1.248.gcd6d1
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 4/6] gettext: Add a Gettext interface for shell scripts

Use GNU's gettext.sh in git-sh-setup if it's available, otherwise fallback on our own custom functions.

A couple of strings in git-pull.sh are now translatable as a proof of concept, including a multiline string.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile        |    7 +++++--
 git-pull.sh     |   15 ++++++++-------
 git-sh-setup.sh |   33 +++++++++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 9 deletions(-)
Show changes to 3 files +46 −9

Makefile, git-pull.sh, git-sh-setup.sh

diff --git a/Makefile b/Makefile
index 4de0627..dce2faa 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 localedir sysconfdir
 
 CC = gcc
 AR = ar
@@ -1455,6 +1456,7 @@ 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))
@@ -1548,6 +1550,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 >$@+
@@ -1881,7 +1884,7 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-pull.sh b/git-pull.sh
index 1a4729f..22a6da2 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -121,8 +121,8 @@ error_on_no_merge_candidates () {
 	do
 		case "$opt" in
 		-t|--t|--ta|--tag|--tags)
-			echo "Fetching tags only, you probably meant:"
-			echo "  git fetch --tags"
+			gettext "Fetching tags only, you probably meant:"; echo
+			gettext "  git fetch --tags"; echo
 			exit 1
 		esac
 	done
@@ -154,11 +154,12 @@ error_on_no_merge_candidates () {
 		echo "a branch. Because this is not the default configured remote"
 		echo "for your current branch, you must specify a branch on the command line."
 	elif [ -z "$curr_branch" ]; then
-		echo "You are not currently on a branch, so I cannot use any"
-		echo "'branch.<branchname>.merge' in your configuration file."
-		echo "Please specify which remote branch you want to use on the command"
-		echo "line and try again (e.g. 'git pull <repository> <refspec>')."
-		echo "See git-pull(1) for details."
+        gettext "You are not currently on a branch, so I cannot use any
+'branch.<branchname>.merge' in your configuration file.
+Please specify which remote branch you want to use on the command
+line and try again (e.g. 'git pull <repository> <refspec>').
+See git-pull(1) for details.";
+        echo
 	elif [ -z "$upstream" ]; then
 		echo "You asked me to pull without telling me which branch you"
 		echo "want to $op_type $op_prep, and 'branch.${curr_branch}.merge' in"
diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 6131670..c8010f2 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -211,3 +211,36 @@ case $(uname -s) in
 	}
 	;;
 esac
+
+# Try to use libintl's gettext.sh, or fall back to English if we
+# can't.
+. gettext.sh
+if test $? -ne 0
+then
+    TEXTDOMAIN=git
+    export TEXDTOMAIN
+    TEXTDOMAINDIR="@@LOCALEDIR@@"
+    export TEXTDOMAINDIR
+else
+    # Since GNU gettext.sh isn't available we'll have to define our
+    # own dummy functions.
+
+    # This code adapted from NessusClient-1.0.2's nessusclient-mkcert
+    # by Michel Arboi <arboi@alussinan.org>. The original code is
+    # under the GPLv2.
+
+    # Not everyone has echo -n
+    case $(echo -n) in
+        \-n)    Xn=   ; Xc='\c' ;;
+        *)      Xn=-n ; Xc=
+    esac
+
+    gettext () {
+        echo $Xn "$1" $Xc
+    }
+
+    eval_gettext () {
+        eval_gettext_var="echo $1"
+        echo $Xn `eval $eval_gettext_var` $Xc
+    }
+fi
-- 
1.7.1.248.gcd6d1
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 5/6] gettext: Add a Gettext interface for Perl

Make Git's gettext messages available to Perl programs through Locale::Messages. Gracefully fall back to English on systems that don't contain the module.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile            |    4 ++-
 git-send-email.perl |    3 +-
 perl/Git/Gettext.pm |   83 +++++++++++++++++++++++++++++++++++++++++++++++++++
 perl/Makefile.PL    |    5 ++-
 4 files changed, 92 insertions(+), 3 deletions(-)
 create mode 100644 perl/Git/Gettext.pm
Show changes to 4 files +92 −3

Makefile, git-send-email.perl, perl/Git/Gettext.pm, perl/Makefile.PL

diff --git a/Makefile b/Makefile
index dce2faa..2101713 100644
--- a/Makefile
+++ b/Makefile
@@ -1884,7 +1884,9 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
+	$(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
+	$(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
+	$(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-send-email.perl b/git-send-email.perl
index 111c981..a36718e 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -26,6 +26,7 @@ use Term::ANSIColor;
 use File::Temp qw/ tempdir tempfile /;
 use Error qw(:try);
 use Git;
+use Git::Gettext qw< :all >;
 
 Getopt::Long::Configure qw/ pass_through /;
 
@@ -674,7 +675,7 @@ if (!defined $sender) {
 	$sender = $repoauthor || $repocommitter || '';
 	$sender = ask("Who should the emails appear to be from? [$sender] ",
 	              default => $sender);
-	print "Emails will be sent from: ", $sender, "\n";
+	printf gettext("Emails will be sent from: %s\n"), $sender;
 	$prompting++;
 }
 
diff --git a/perl/Git/Gettext.pm b/perl/Git/Gettext.pm
new file mode 100644
index 0000000..f434783
--- /dev/null
+++ b/perl/Git/Gettext.pm
@@ -0,0 +1,83 @@
+package Git::Gettext;
+use strict;
+use warnings;
+use Exporter;
+use base 'Exporter';
+
+our $VERSION = '0.01';
+
+our @EXPORT;
+our @EXPORT_OK = qw< gettext >;
+our %EXPORT_TAGS;
+@{ $EXPORT_TAGS{'all'} } = @EXPORT_OK;
+
+sub __bootstrap_locale_messages {
+	our $TEXTDOMAIN = 'git';
+
+	# TODO: How do I make the sed replacements in the top level
+	# Makefile reach me here?
+	#our $TEXTDOMAINDIR = q|@@LOCALEDIR@@|;
+	our $TEXTDOMAINDIR = q</usr/local/share/locale>;
+
+	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
+		*gettext = sub ($) { $_[0] };
+	}
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Git::Gettext - Perl interface to Git's Gettext localizations
+
+=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 gettext($)
+
+L<Locale::Messages>'s gettext function if all goes well, otherwise our
+passthrough fallback function.
+
+=head1 EXPORTS
+
+Exports are done via L<Exporter>. Invididual functions can be
+exporter, or all of them via the C<:all> export tag.
+
+=head1 AUTHOR
+
+E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright 2010 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
+
+This program is free software, you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
diff --git a/perl/Makefile.PL b/perl/Makefile.PL
index 0b9deca..702ec7c 100644
--- a/perl/Makefile.PL
+++ b/perl/Makefile.PL
@@ -16,7 +16,10 @@ endif
 MAKE_FRAG
 }
 
-my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm');
+my %pm = (
+	'Git.pm' => '$(INST_LIBDIR)/Git.pm',
+	'Git/Gettext.pm' => '$(INST_LIBDIR)/Git/Gettext.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.
-- 
1.7.1.248.gcd6d1
Jakub Narebski· Jun 1, 2010, 17:00 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v2 5/6] gettext: Add a Gettext interface for Perl

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
> Make Git's gettext messages available to Perl programs through
> Locale::Messages. Gracefully fall back to English on systems that
> don't contain the module.

This is I think a very good idea, both providing wrapper with fallback to untranslated messages (and encapsulating translation), and using the Locale::Messages module from libintl-perl library. The "On the state of i18n in Perl" (http://rassie.org/archives/247) blog post from 26 April 2009 provides nice counterpoint to Locale::Maketext::TPJ13 / The Perl Journal #13 article[1] from 1999... especially because using gettext is natural for translating git command output and GUI, because git uses it already for Tcl/Tk (gitk and git-gui), and it is natural solution for code in C, which slightly less than half of git code.

Well, we could use Locale::Maketext::Gettext, but it is not in Perl core either, and as http://rassie.org/archives/247 says its '*.po' files are less natural. The gettext documentation (gettext.info) also recommends libintl-perl, or to be more exact Locale::TextDomain from it.

[1] http://search.cpan.org/perldoc?Locale::Maketext::TPJ13
    http://interglacial.com/~sburke/tpj/as_html/tpj13.html

The question is why not use Locale::TextDomain, the high-level Perl-y framework, wrapper around Locale::Messages from the same libintl-perl library? The gettext documentation (in gettext.info, chapter "13.5.18 Perl") says:

  Prerequisite
     `use POSIX;'
     `use Locale::TextDomain;' (included in the package libintl-perl
     which is available on the Comprehensive Perl Archive Network CPAN,
     http://www.cpan.org/).
This would change
 -	print "Emails will be sent from: ", $sender, "\n";
 +	printf gettext("Emails will be sent from: %s\n"), $sender;
to either
 +	print __"Emails will be sent from: ", $sender, "\n";
or
 +	printf __("Emails will be sent from: %s\n"), $sender;
or
 +	print __x("Emails will be sent from: {sender}\n",
 +	          sender => $sender);
Show 21 quoted lines
> 
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  Makefile            |    4 ++-
>  git-send-email.perl |    3 +-
>  perl/Git/Gettext.pm |   83 +++++++++++++++++++++++++++++++++++++++++++++++++++
>  perl/Makefile.PL    |    5 ++-
>  4 files changed, 92 insertions(+), 3 deletions(-)
>  create mode 100644 perl/Git/Gettext.pm
> 
> diff --git a/Makefile b/Makefile
> index dce2faa..2101713 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1884,7 +1884,9 @@ cscope:
>  	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
>  
>  pot:
> -	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
> +	$(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
> +	$(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)

Shouldn't this line be in earlier patch, i.e. in "gettext: Add a Gettext interface for shell scripts"?

> +	$(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
From gettext documentation (in gettext.info, chapter "13.5.18 Perl"):
  Extractor
     `xgettext -k__ -k\$__ -k%__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2
     -kN__ -k'
Is it equivalent to specifying 'xgettext --language=Perl'?

Of course the above assumes that you are using Locale::TextDomain, or at least use the same conventions.

Show 12 quoted lines
>  
>  POFILES := $(wildcard po/*.po)
>  MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
> diff --git a/git-send-email.perl b/git-send-email.perl
> index 111c981..a36718e 100755
> --- a/git-send-email.perl
> +++ b/git-send-email.perl
> @@ -26,6 +26,7 @@ use Term::ANSIColor;
>  use File::Temp qw/ tempdir tempfile /;
>  use Error qw(:try);
>  use Git;
> +use Git::Gettext qw< :all >;
Please follow the existing convention, i.e.
  +use Git::Gettext qw(:all);
like you see in context line for 'use Error'.

Well, not that git-send-email.perl is very consistent about this issue, see 'use File::Temp qw/ tempdir tempfile /;' versus 'use Error qw(:try);' but I'd reather you didn't introduce yet another form.

Show 9 quoted lines
>  
>  Getopt::Long::Configure qw/ pass_through /;
>  
> @@ -674,7 +675,7 @@ if (!defined $sender) {
>  	$sender = $repoauthor || $repocommitter || '';
>  	$sender = ask("Who should the emails appear to be from? [$sender] ",
>  	              default => $sender);
> -	print "Emails will be sent from: ", $sender, "\n";
> +	printf gettext("Emails will be sent from: %s\n"), $sender;
As I wrote, this IMHO should be either
  +	printf __("Emails will be sent from: %s\n"), $sender;
or
  +	print __x("Emails will be sent from: {sender}\n", sender => $sender);
(note that parantheses differ in those two examples).
Show 10 quoted lines
>  	$prompting++;
>  }
>  
> diff --git a/perl/Git/Gettext.pm b/perl/Git/Gettext.pm
> new file mode 100644
> index 0000000..f434783
> --- /dev/null
> +++ b/perl/Git/Gettext.pm
> @@ -0,0 +1,83 @@
> +package Git::Gettext;

Should this package be named Git::Gettext, or other name would be better, perhaps Git::I18N (like e.g. Games::Risk have Games::Risk::I18N), or Git::Locale, or even Git::Translator?

Not very important.
> +use strict;
> +use warnings;
> +use Exporter;
> +use base 'Exporter';
O.K.
The alternative would be to use
  +use Exporter qw(import);
Show 5 quoted lines
> +
> +our $VERSION = '0.01';
> +
> +our @EXPORT;
> +our @EXPORT_OK = qw< gettext >;

Same comment as above: more common way is to use qw(gettext) not qw< gettext >; it is also the notation used in Exporter documentation.

> +our %EXPORT_TAGS;
> +@{ $EXPORT_TAGS{'all'} } = @EXPORT_OK;
Why not simply
  +our %EXPORT_TAGS = ('all' => [ @EXPORT_OK ]);
or the reverse
  +our %EXPORT_TAGS = ('all' => [qw(gettext)]);
  +our @EXPORT_OK   = @{$EXPORT_TAGS{'all'}};
or the reverse using tag utility functions
  +our %EXPORT_TAGS = ('all' => [qw(gettext)]);
  +Exporter::export_ok_tags('all');
Show 7 quoted lines
> +
> +sub __bootstrap_locale_messages {
> +	our $TEXTDOMAIN = 'git';
> +
> +	# TODO: How do I make the sed replacements in the top level
> +	# Makefile reach me here?
> +	#our $TEXTDOMAINDIR = q|@@LOCALEDIR@@|;

In Perl (well, in gitweb/gitweb.perl) we use '++VAR++' and not '@@VAR@@' for placeholders, because '@' is sigil in Perl. This is not important in above example, because it is not interpolated string.

Make invoked on perl/Makefile, when invoked from main Makefile by '$(MAKE) -C perl' (via QUIET_SUBDIR0) passes 'localedir' to submake; perl/Makefile should probably have something like

  localedir ?= $(sharedir)/locale

That is assuming that 'localedir' is added to list of exported variables.

But I am not sure how such substitution should be performed.
> +	our $TEXTDOMAINDIR = q</usr/local/share/locale>;
Why q<...> and not simply '...'?
Show 14 quoted lines
> +
> +	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;
> +}
This would probably be a bit simpler with Locale::TextDomain.
Show 8 quoted lines
> +
> +BEGIN
> +{
> +	local ($@, $!);
> +	eval { __bootstrap_locale_messages() };
> +	if ($@) {
> +		# Oh noes, no Locale::Messages here
> +		*gettext = sub ($) { $_[0] };

Do you intended to use subroutine protytype here? Ah, I see that Locale::Messages::gettext has the same prototype...

> +	}
> +}
Does it need to be in BEGIN block?  Probably yes.
Show 5 quoted lines
> +
> +1;
> +
> +__END__
> +
It's nice that you have provided documentation.
Show 9 quoted lines
> +=head1 NAME
> +
> +Git::Gettext - Perl interface to Git's Gettext localizations
> +
> +=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.
Very good.

It would probably be better though to use L<Locale::TextDomain> instead of low(er)-level L<Locale::Messages>.

Show 7 quoted lines
> +
> +=head1 FUNCTIONS
> +
> +=head2 gettext($)
> +
> +L<Locale::Messages>'s gettext function if all goes well, otherwise our
> +passthrough fallback function.

Other packages use _T() function for that, or like Locale::TextDomain __() function.

Show 5 quoted lines
> +
> +=head1 EXPORTS
> +
> +Exports are done via L<Exporter>. Invididual functions can be
> +exporter, or all of them via the C<:all> export tag.

Shouldn't this be described in less technical way in SYNOPSIS and DESCRIPTION sections instead?

Show 11 quoted lines
> +
> +=head1 AUTHOR
> +
> +E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
> +
> +=head1 LICENSE AND COPYRIGHT
> +
> +Copyright 2010 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
> +
> +This program is free software, you can redistribute it and/or modify
> +it under the same terms as Perl itself.
Which is dual licensed: GPL + Perl artistic.

Was it intended to use 'same terms as Perl itself' rather than GPLv2 or GPLv2+?

Show 21 quoted lines
> +
> +=cut
> diff --git a/perl/Makefile.PL b/perl/Makefile.PL
> index 0b9deca..702ec7c 100644
> --- a/perl/Makefile.PL
> +++ b/perl/Makefile.PL
> @@ -16,7 +16,10 @@ endif
>  MAKE_FRAG
>  }
>  
> -my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm');
> +my %pm = (
> +	'Git.pm' => '$(INST_LIBDIR)/Git.pm',
> +	'Git/Gettext.pm' => '$(INST_LIBDIR)/Git/Gettext.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.
> -- 
> 1.7.1.248.gcd6d1
> 
-- 
Jakub Narebski
Poland
ShadeHawk on #git
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 19:06 UTC · re: Jakub Narebski · lore

Re: [PATCH/RFC v2 5/6] gettext: Add a Gettext interface for Perl

On Tue, Jun 1, 2010 at 17:00, Jakub Narebski <jnareb@gmail.com> wrote:
Show 25 quoted lines
>
> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> Make Git's gettext messages available to Perl programs through
>> Locale::Messages. Gracefully fall back to English on systems that
>> don't contain the module.
>
> This is I think a very good idea, both providing wrapper with fallback
> to untranslated messages (and encapsulating translation), and using the
> Locale::Messages module from libintl-perl library.  The "On the state of
> i18n in Perl" (http://rassie.org/archives/247) blog post from 26 April
> 2009 provides nice counterpoint to Locale::Maketext::TPJ13 / The Perl
> Journal #13 article[1] from 1999... especially because using gettext is
> natural for translating git command output and GUI, because git uses it
> already for Tcl/Tk (gitk and git-gui), and it is natural solution for
> code in C, which slightly less than half of git code.
>
> Well, we could use Locale::Maketext::Gettext, but it is not in Perl
> core either, and as http://rassie.org/archives/247 says its '*.po'
> files are less natural.  The gettext documentation (gettext.info) also
> recommends libintl-perl, or to be more exact Locale::TextDomain from
> it.
>
> [1] http://search.cpan.org/perldoc?Locale::Maketext::TPJ13
>    http://interglacial.com/~sburke/tpj/as_html/tpj13.html

Locale::Maketext is a less complete and non-standard alternative as the above blogs note. I think the main reason it's used in favor of Gettext in many Perl projects is that it can be compiled stand-alone from the CPAN, i.e. it doesn't depend on libintl.

That's comfortable when CPAN is your main distribution channel, but Git's main distribution channel is via OS packages, which can trivially introduce a gettext dependency (for which they doubtless already have a package).

Locale::Maketext has a Gettext emulation layer, but using it would be a potential source of bugs (no emulation is perfect).

That being said the way I wrote Git::Gexttext means any alterante implementation or emulation layer can be seemlessly added later on. We could use (or write) something for Perl, C or Shell that completely bypasses libintl for systems that don't have a gettext C library.

Show 29 quoted lines
> The question is why not use Locale::TextDomain, the high-level Perl-y
> framework, wrapper around Locale::Messages from the same libintl-perl
> library?  The gettext documentation (in gettext.info, chapter "13.5.18
> Perl") says:
>
>  Prerequisite
>     `use POSIX;'
>     `use Locale::TextDomain;' (included in the package libintl-perl
>     which is available on the Comprehensive Perl Archive Network CPAN,
>     http://www.cpan.org/).
>
>
> This would change
>
>  -      print "Emails will be sent from: ", $sender, "\n";
>  +      printf gettext("Emails will be sent from: %s\n"), $sender;
>
> to either
>
>  +      print __"Emails will be sent from: ", $sender, "\n";
>
> or
>
>  +      printf __("Emails will be sent from: %s\n"), $sender;
>
> or
>
>  +      print __x("Emails will be sent from: {sender}\n",
>  +                sender => $sender);

I didn't use Locale::TextDomain because it exports a fatter interface by default, and I'm not sure at this point what subset of it it would make sense to support.

This is the default Locale::TexdDomain interface:
   @EXPORT = qw (__ __x __n __nx __xn __p __px __np __npx $__ %__
                 N__ N__n N__p N__np);

My Git::Gettext only exports a single gettext() function now. I think it's better at this point to have a really small interface and decide later if we'd like to expand it, preferably in a way that'll work consistently across C, Perl and Shell programs.

We might want to improve plural support, add msgctxt etc. later. But for an initial implementation I'd rather have something simple & stupid.

Show 23 quoted lines
>> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
>> ---
>>  Makefile            |    4 ++-
>>  git-send-email.perl |    3 +-
>>  perl/Git/Gettext.pm |   83 +++++++++++++++++++++++++++++++++++++++++++++++++++
>>  perl/Makefile.PL    |    5 ++-
>>  4 files changed, 92 insertions(+), 3 deletions(-)
>>  create mode 100644 perl/Git/Gettext.pm
>>
>> diff --git a/Makefile b/Makefile
>> index dce2faa..2101713 100644
>> --- a/Makefile
>> +++ b/Makefile
>> @@ -1884,7 +1884,9 @@ cscope:
>>       $(FIND) . -name '*.[hcS]' -print | xargs cscope -b
>>
>>  pot:
>> -     $(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
>> +     $(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
>> +     $(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
>
> Shouldn't this line be in earlier patch, i.e. in "gettext: Add a
> Gettext interface for shell scripts"?
It needed to be split up into seperate line in the Perl commit because ...
Show 9 quoted lines
>> +     $(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
>
> From gettext documentation (in gettext.info, chapter "13.5.18 Perl"):
>
>  Extractor
>     `xgettext -k__ -k\$__ -k%__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2
>     -kN__ -k'
>
> Is it equivalent to specifying 'xgettext --language=Perl'?

I'm doing --language=Perl because gettext doesn't recognize .perl as a valid Perl extension. It only knows about `pl', `PL', `pm', `cgi'. (As an aside, I haven't found why Git chose to use the quaint .perl extension).

But yeah, using --language=Perl is overshooting it with regards to keyword extraction. From "5.1 Invoking the `xgettext' Program":

     To disable the default keyword specifications, the option `-k' or
     `--keyword' or `--keyword=', without a KEYWORDSPEC, can be used.
I'll check if I can supply --keyword to all the XGETTEXT invocations.
Show 27 quoted lines
> Of course the above assumes that you are using Locale::TextDomain, or
> at least use the same conventions.
>
>>
>>  POFILES := $(wildcard po/*.po)
>>  MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
>> diff --git a/git-send-email.perl b/git-send-email.perl
>> index 111c981..a36718e 100755
>> --- a/git-send-email.perl
>> +++ b/git-send-email.perl
>> @@ -26,6 +26,7 @@ use Term::ANSIColor;
>>  use File::Temp qw/ tempdir tempfile /;
>>  use Error qw(:try);
>>  use Git;
>> +use Git::Gettext qw< :all >;
>
> Please follow the existing convention, i.e.
>
>  +use Git::Gettext qw(:all);
>
> like you see in context line for 'use Error'.
>
>
> Well, not that git-send-email.perl is very consistent about this
> issue, see 'use File::Temp qw/ tempdir tempfile /;' versus
> 'use Error qw(:try);' but I'd reather you didn't introduce yet
> another form.

OK. I'll change that. I usually just pick whatever Perl quote delimiters that happen to, literally, be close at hand at the time :)

Show 18 quoted lines
>>  Getopt::Long::Configure qw/ pass_through /;
>>
>> @@ -674,7 +675,7 @@ if (!defined $sender) {
>>       $sender = $repoauthor || $repocommitter || '';
>>       $sender = ask("Who should the emails appear to be from? [$sender] ",
>>                     default => $sender);
>> -     print "Emails will be sent from: ", $sender, "\n";
>> +     printf gettext("Emails will be sent from: %s\n"), $sender;
>
> As I wrote, this IMHO should be either
>
>  +     printf __("Emails will be sent from: %s\n"), $sender;
>
> or
>
>  +     print __x("Emails will be sent from: {sender}\n", sender => $sender);
>
> (note that parantheses differ in those two examples).

I'd prefer the former argument expansion form (i.e. %s, not {$sender}), just so translators don't have to deal with two different argument forms.

As for __ or gettext I don't mind. I just picked the former on a whim to match the Shell version. Maybe I should just change C|Perl|Shell to use _?

And we can use _, __ is just used to disambiguate it from the _ filehandle, and Perl's pretty good at disambiguating that already:

    $ perl -E 'sub _ ($) { scalar reverse $_[0] }; say _("moo"); stat
"/etc/hosts"; say ((stat(_))[7])'
    oom
    168
Show 16 quoted lines
>>       $prompting++;
>>  }
>>
>> diff --git a/perl/Git/Gettext.pm b/perl/Git/Gettext.pm
>> new file mode 100644
>> index 0000000..f434783
>> --- /dev/null
>> +++ b/perl/Git/Gettext.pm
>> @@ -0,0 +1,83 @@
>> +package Git::Gettext;
>
> Should this package be named Git::Gettext, or other name would be
> better, perhaps Git::I18N (like e.g. Games::Risk have
> Games::Risk::I18N), or Git::Locale, or even Git::Translator?
>
> Not very important.

I really have no opinion on that, but it does seem like a lot of Perl packages on CPAN use the ::I18N suffix.

Show 10 quoted lines
>> +use strict;
>> +use warnings;
>> +use Exporter;
>> +use base 'Exporter';
>
> O.K.
>
> The alternative would be to use
>
>  +use Exporter qw(import);

Not if we want to maintain 5.6 support. The `use Exporter "import"' form was only introduced in 5.8. I use it everywhere where I don't have to care about 5.6 support (which is everywhere but Git).

Show 8 quoted lines
>> +
>> +our $VERSION = '0.01';
>> +
>> +our @EXPORT;
>> +our @EXPORT_OK = qw< gettext >;
>
> Same comment as above: more common way is to use qw(gettext) not
> qw< gettext >; it is also the notation used in Exporter documentation.
Will do.
Show 16 quoted lines
>> +our %EXPORT_TAGS;
>> +@{ $EXPORT_TAGS{'all'} } = @EXPORT_OK;
>
> Why not simply
>
>  +our %EXPORT_TAGS = ('all' => [ @EXPORT_OK ]);
>
> or the reverse
>
>  +our %EXPORT_TAGS = ('all' => [qw(gettext)]);
>  +our @EXPORT_OK   = @{$EXPORT_TAGS{'all'}};
>
> or the reverse using tag utility functions
>
>  +our %EXPORT_TAGS = ('all' => [qw(gettext)]);
>  +Exporter::export_ok_tags('all');

Just because it some old code of mine I had around that I copied from. I could rewrite it to a nicer form.

Actually I was considering changing it to just have @EXPORT and nothing else. You're going to use gettext if you use the medule anyway, so we might as well just import them all everywhere we use Gettext in Perl.

Show 11 quoted lines
>> +
>> +sub __bootstrap_locale_messages {
>> +     our $TEXTDOMAIN = 'git';
>> +
>> +     # TODO: How do I make the sed replacements in the top level
>> +     # Makefile reach me here?
>> +     #our $TEXTDOMAINDIR = q|@@LOCALEDIR@@|;
>
> In Perl (well, in gitweb/gitweb.perl) we use '++VAR++' and not
> '@@VAR@@' for placeholders, because '@' is sigil in Perl.  This is not
> important in above example, because it is not interpolated string.
I'll change it to follow that convention.
Show 10 quoted lines
> Make invoked on perl/Makefile, when invoked from main Makefile by
> '$(MAKE) -C perl' (via QUIET_SUBDIR0) passes 'localedir' to submake;
> perl/Makefile should probably have something like
>
>  localedir ?= $(sharedir)/locale
>
> That is assuming that 'localedir' is added to list of exported
> variables.
>
> But I am not sure how such substitution should be performed.

I'll try to get something like that to work. Actually the main problem seemed to be that it had a hybrid handwritten Makefile and one made by EU::MM.

>> +     our $TEXTDOMAINDIR = q</usr/local/share/locale>;
>
> Why q<...> and not simply '...'?

Same reason I use qw<>, that is no particular reason other than it being easier to type on my keyboard. I'll just change it to ''.

Show 27 quoted lines
>> +     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;
>> +}
>
> This would probably be a bit simpler with Locale::TextDomain.
>
>> +
>> +BEGIN
>> +{
>> +     local ($@, $!);
>> +     eval { __bootstrap_locale_messages() };
>> +     if ($@) {
>> +             # Oh noes, no Locale::Messages here
>> +             *gettext = sub ($) { $_[0] };
>
> Do you intended to use subroutine protytype here?  Ah, I see that
> Locale::Messages::gettext has the same prototype...
Yeah, otherwise the fallback would have different semantics.
>> +     }
>> +}
>
> Does it need to be in BEGIN block?  Probably yes.

I'm pretty sure it needs to all be at BEGIN time. I recall that declaring prototypes at runtime was an error in some older versions, but it works now:

    $ perl -E '*meh = sub ($) { "foo" }; say prototype "meh"'
    $

Anyway since it's code that's being use'd it's going to always be at BEGIN time anyway. I just wanted to be explicit.

Show 42 quoted lines
>> +
>> +1;
>> +
>> +__END__
>> +
>
> It's nice that you have provided documentation.
>
>> +=head1 NAME
>> +
>> +Git::Gettext - Perl interface to Git's Gettext localizations
>> +
>> +=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.
>
> Very good.
>
> It would probably be better though to use L<Locale::TextDomain>
> instead of low(er)-level L<Locale::Messages>.
>
>> +
>> +=head1 FUNCTIONS
>> +
>> +=head2 gettext($)
>> +
>> +L<Locale::Messages>'s gettext function if all goes well, otherwise our
>> +passthrough fallback function.
>
> Other packages use _T() function for that, or like Locale::TextDomain
> __() function.
>
>> +
>> +=head1 EXPORTS
>> +
>> +Exports are done via L<Exporter>. Invididual functions can be
>> +exporter, or all of them via the C<:all> export tag.
>
> Shouldn't this be described in less technical way in SYNOPSIS and
> DESCRIPTION sections instead?

Yeah, maybe. But there's a convention for `=head1 EXPORTS' in perl core / CPAN. So that's what I added at a whim.

Show 16 quoted lines
>> +
>> +=head1 AUTHOR
>> +
>> +E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
>> +
>> +=head1 LICENSE AND COPYRIGHT
>> +
>> +Copyright 2010 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avarab@gmail.com>
>> +
>> +This program is free software, you can redistribute it and/or modify
>> +it under the same terms as Perl itself.
>
> Which is dual licensed: GPL + Perl artistic.
>
> Was it intended to use 'same terms as Perl itself' rather than GPLv2
> or GPLv2+?

It's the permissive boilerplate I auto-insert for Perl code. I'll just remove it. The COPYING file at the top level will suffice.

Show 21 quoted lines
>> +
>> +=cut
>> diff --git a/perl/Makefile.PL b/perl/Makefile.PL
>> index 0b9deca..702ec7c 100644
>> --- a/perl/Makefile.PL
>> +++ b/perl/Makefile.PL
>> @@ -16,7 +16,10 @@ endif
>>  MAKE_FRAG
>>  }
>>
>> -my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm');
>> +my %pm = (
>> +     'Git.pm' => '$(INST_LIBDIR)/Git.pm',
>> +     'Git/Gettext.pm' => '$(INST_LIBDIR)/Git/Gettext.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.
>> --
>> 1.7.1.248.gcd6d1
>>
Jakub Narebski· Jun 2, 2010, 11:47 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v2 5/6] gettext: Add a Gettext interface for Perl

On Tue, 1 June 2010, Ævar Arnfjörð Bjarmason wrote:
Show 28 quoted lines
> On Tue, Jun 1, 2010 at 17:00, Jakub Narebski <jnareb@gmail.com> wrote:
>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>
>>> Make Git's gettext messages available to Perl programs through
>>> Locale::Messages. [...]
>>
>> This is I think a very good idea [...] using the
>> Locale::Messages module from libintl-perl library.  The "On the state of
>> i18n in Perl" (http://rassie.org/archives/247) blog post from 26 April
>> 2009 provides nice counterpoint to Locale::Maketext::TPJ13 / The Perl
>> Journal #13 article[1] from 1999... especially because using gettext is
>> natural for translating git command output and GUI, because git uses it
>> already for Tcl/Tk (gitk and git-gui), and it is natural solution for
>> code in C, which slightly less than half of git code.
>>
>> Well, we could use Locale::Maketext::Gettext, but it is not in Perl
>> core either, and as http://rassie.org/archives/247 says its '*.po'
>> files are less natural.  The gettext documentation (gettext.info) also
>> recommends libintl-perl, or to be more exact Locale::TextDomain from
>> it.
>>
>> [1] http://search.cpan.org/perldoc?Locale::Maketext::TPJ13
>>     http://interglacial.com/~sburke/tpj/as_html/tpj13.html
> 
> Locale::Maketext is a less complete and non-standard alternative as
> the above blogs note. I think the main reason it's used in favor of
> Gettext in many Perl projects is that it can be compiled stand-alone
> from the CPAN, i.e. it doesn't depend on libintl.

That, and the fact that some Perl projects / modules on CPAN started using Locale::Maketext before gettext acquired functionality for handling plural forms like ngettext. The mentioned article from The Perl Journal #13 might have also mislead module authors...

On the other hand libintl-perl distribution contains pure-Perl gettext emulation module: Locale::gettext_pp.

Show 5 quoted lines
> 
> That's comfortable when CPAN is your main distribution channel, but
> Git's main distribution channel is via OS packages, which can
> trivially introduce a gettext dependency (for which they doubtless
> already have a package).
We need gettext for C, for shell, and for Tcl/Tk anyway.

BTW. some projects keep copy of libintl / gettext library in 'intl/' subdirectory, to be used if it is not present in the target system. Perl has 'inc/' and 'use inc::latest' (unfortunately in core only since 5.011002, i.e. 5.12 in practice) to keep private copy of modules to be used / installed if they are not present in system.

> 
> Locale::Maketext has a Gettext emulation layer, but using it would be
> a potential source of bugs (no emulation is perfect).

OTOH see Locale::gettext_pp from libintl-perl (the distribution containing Locale::Messages and Locale::TextDomain modules).

Show 5 quoted lines
> 
> That being said the way I wrote Git::Gexttext means any alterante
> implementation or emulation layer can be seemlessly added later on. We
> could use (or write) something for Perl, C or Shell that completely
> bypasses libintl for systems that don't have a gettext C library.
Right.

Besides I think that the base gettext (no plural forms etc.) is the same (would produce the same *.po files) for Locale::Maketext::Gettext and for Locale::Messages / Locale::TextDomain.

Show 44 quoted lines
> 
>> The question is why not use Locale::TextDomain, the high-level Perl-y
>> framework, wrapper around Locale::Messages from the same libintl-perl
>> library?  The gettext documentation (in gettext.info, chapter "13.5.18
>> Perl") says:
>>
>>  Prerequisite
>>     `use POSIX;'
>>     `use Locale::TextDomain;' (included in the package libintl-perl
>>     which is available on the Comprehensive Perl Archive Network CPAN,
>>     http://www.cpan.org/).
>>
>>
>> This would change
>>
>>  -      print "Emails will be sent from: ", $sender, "\n";
>>  +      printf gettext("Emails will be sent from: %s\n"), $sender;
>>
>> to either
>>
>>  +      print __"Emails will be sent from: ", $sender, "\n";
>>
>> or
>>
>>  +      printf __("Emails will be sent from: %s\n"), $sender;
>>
>> or
>>
>>  +      print __x("Emails will be sent from: {sender}\n",
>>  +                sender => $sender);
> 
> I didn't use Locale::TextDomain because it exports a fatter interface
> by default, and I'm not sure at this point what subset of it it would
> make sense to support.
> 
> This is the default Locale::TextDomain interface:
> 
>    @EXPORT = qw (__ __x __n __nx __xn __p __px __np __npx $__ %__
>                  N__ N__n N__p N__np);
> 
> My Git::Gettext only exports a single gettext() function now. I think
> it's better at this point to have a really small interface and decide
> later if we'd like to expand it, preferably in a way that'll work
> consistently across C, Perl and Shell programs.

That is not, I think, the problem. You don't need to re-export _all_ of Locale::TextDomain interface (or create and export stubs for all above functions and variables).

> 
> We might want to improve plural support, add msgctxt etc. later. But
> for an initial implementation I'd rather have something simple &
> stupid.
That's understandable.

BTW. I wonder if git currently has anywhere mesage that depends on plural form, or does it always use plural / multiform, like for example '1 files changed' from "git diff --summary"...

Show 39 quoted lines
> 
>>> Signed-off-by: ?var Arnfjör? Bjarmason <avarab@gmail.com>
>>> ---
>>>  Makefile            |    4 ++-
>>>  git-send-email.perl |    3 +-
>>>  perl/Git/Gettext.pm |   83 +++++++++++++++++++++++++++++++++++++++++++++++++++
>>>  perl/Makefile.PL    |    5 ++-
>>>  4 files changed, 92 insertions(+), 3 deletions(-)
>>>  create mode 100644 perl/Git/Gettext.pm
>>>
>>> diff --git a/Makefile b/Makefile
>>> index dce2faa..2101713 100644
>>> --- a/Makefile
>>> +++ b/Makefile
>>> @@ -1884,7 +1884,9 @@ cscope:
>>>       $(FIND) . -name '*.[hcS]' -print | xargs cscope -b
>>>
>>>  pot:
>>> -     $(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
>>> +     $(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
>>> +     $(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
>>
>> Shouldn't this line be in earlier patch, i.e. in "gettext: Add a
>> Gettext interface for shell scripts"?
> 
> It needed to be split up into seperate line in the Perl commit because ...
> 
>>> +     $(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
>>
>> From gettext documentation (in gettext.info, chapter "13.5.18 Perl"):
>>
>>  Extractor
>>     `xgettext -k__ -k\$__ -k%__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2
>>     -kN__ -k'
>>
>> Is it equivalent to specifying 'xgettext --language=Perl'?
> 
> I'm doing --language=Perl because gettext doesn't recognize .perl as a
> valid Perl extension. It only knows about `pl', `PL', `pm', `cgi'.
All right.
Show 13 quoted lines
> (As an aside, I haven't found why Git chose to use the quaint .perl
> extension).
> 
> But yeah, using --language=Perl is overshooting it with regards to
> keyword extraction. From "5.1 Invoking the `xgettext' Program":
> 
>      To disable the default keyword specifications, the option `-k' or
>      `--keyword' or `--keyword=', without a KEYWORDSPEC, can be used.
> 
> I'll check if I can supply --keyword to all the XGETTEXT invocations.
> 
>> Of course the above assumes that you are using Locale::TextDomain, or
>> at least use the same conventions.
In my opinion we should use the common convention for the programming
language, like described in chapter 15 "Other Programming Languages"
in gettext manual: 
  http://www.gnu.org/software/gettext/manual/gettext.html#Programming-Languages
 * C:      _("msg")
 * Perl:   __"msg"
 * shell:  $(gettext "abc")
 * Tcl/Tk: [_ "msg"]
 * Python: _('msg')

On the other hand the only currently traslated part of git, namely gitk and git-gui, both of which are written in Tcl/Tk, do not use recommended gettext shorthand, i.e. [_ "msg"], but the gettext function directly, i.e. [mc "msg"] (in the imported form, and not [::msgcat::mc "msg"]). I guess this is caused by the fact that shorthand is not that shorter here...

Show 22 quoted lines
>>>  Getopt::Long::Configure qw/ pass_through /;
>>>
>>> @@ -674,7 +675,7 @@ if (!defined $sender) {
>>>       $sender = $repoauthor || $repocommitter || '';
>>>       $sender = ask("Who should the emails appear to be from? [$sender] ",
>>>                     default => $sender);
>>> -     print "Emails will be sent from: ", $sender, "\n";
>>> +     printf gettext("Emails will be sent from: %s\n"), $sender;
>>
>> As I wrote, this IMHO should be either
>>
>>  +     printf __("Emails will be sent from: %s\n"), $sender;
>>
>> or
>>
>>  +     print __x("Emails will be sent from: {sender}\n", sender => $sender);
>>
>> (note that parantheses differ in those two examples).
> 
> I'd prefer the former argument expansion form (i.e. %s, not
> {$sender}), just so translators don't have to deal with two different
> argument forms.

Well, in this case this does not matter (much), because we have only single placeholder.

The Locale::TextDomain manpage says that the reason behind __x() is twofold. First, you would need context to know what placeholder means in

  printf __"This is the %s %s.\n", $color, $thing;
as you would have only
  msgid "This is the %s %s.\n"

in the PO file. Second, formatting with position (to change order of parameters) is possible with 'printf' only for Perl 5.8.0 or never:

  printf __("This is the %1\$s %2\$s.\n"), $color, $thing;
So that's the reason for introducing
  print __x("This is the {color} {thing}.\n",
            color => $color, thing => $thing);
Show 11 quoted lines
> As for __ or gettext I don't mind. I just picked the former on a whim
> to match the Shell version. Maybe I should just change C|Perl|Shell to
> use _?
> 
> And we can use _, __ is just used to disambiguate it from the _
> filehandle, and Perl's pretty good at disambiguating that already:
> 
>     $ perl -E 'sub _ ($) { scalar reverse $_[0] }; say _("moo"); stat
> "/etc/hosts"; say ((stat(_))[7])'
>     oom
>     168
Locale::TextDomain::FAQ says:
 Q. Why does Locale::TextDomain use a double underscore? I am used to
    a single underscore from C or other languages.
 A. Function names that consist of exactly one non-alphanumerical
    character make the function automatically global in Perl. Besides,
    in Perl 6 the concatenation operator will be the underscore
    instead of the dot.
    [jn: actually the string concatenation operator in Perl 6 is '~',
     according to http://perlgeek.de/en/article/5-to-6#post_11: 
     "Perl 5 to 6" Lesson 11 - Changes to Perl 5 Operators]
Although 'gettext-tools/examples/hello-perl/hello-1.pl.in' from
gettext sources
  http://git.savannah.gnu.org/cgit/gettext.git/tree/gettext-tools/examples/hello-perl/hello-1.pl.in
(hello-1 uses Locale::Messages, hello-2 uses Locale::TextDomain)
uses _("msg") shortcut:
  sub _ ($) { &gettext; }
Hmmm...
Show 16 quoted lines
>>> diff --git a/perl/Git/Gettext.pm b/perl/Git/Gettext.pm
>>> new file mode 100644
>>> index 0000000..f434783
>>> --- /dev/null
>>> +++ b/perl/Git/Gettext.pm
>>> @@ -0,0 +1,83 @@
>>> +package Git::Gettext;
>>
>> Should this package be named Git::Gettext, or other name would be
>> better, perhaps Git::I18N (like e.g. Games::Risk have
>> Games::Risk::I18N), or Git::Locale, or even Git::Translator?
>>
>> Not very important.
> 
> I really have no opinion on that, but it does seem like a lot of Perl
> packages on CPAN use the ::I18N suffix.

Also using Git::I18N allows us to change the backend from gettext to something other (like Locale::Maketext lexicon, or something yet another).

Show 14 quoted lines
>>> +use strict;
>>> +use warnings;
>>> +use Exporter;
>>> +use base 'Exporter';
>>
>> O.K.
>>
>> The alternative would be to use
>>
>>  +use Exporter qw(import);
> 
> Not if we want to maintain 5.6 support. The `use Exporter "import"'
> form was only introduced in 5.8. I use it everywhere where I don't
> have to care about 5.6 support (which is everywhere but Git).
Oh, all right.  Thanks for explanation.
Show 6 quoted lines
>>> +our %EXPORT_TAGS;
>>> +@{ $EXPORT_TAGS{'all'} } = @EXPORT_OK;
>>
>> Why not simply
>>
>>  +our %EXPORT_TAGS = ('all' => [ @EXPORT_OK ]);
[...]
Show 8 quoted lines
> 
> Just because it some old code of mine I had around that I copied
> from. I could rewrite it to a nicer form.
> 
> Actually I was considering changing it to just have @EXPORT and
> nothing else. You're going to use gettext if you use the module
> anyway, so we might as well just import them all everywhere we use
> Gettext in Perl.

Right, if we use Git::Gettext / Git::I18N it means that we want to do message traslation, so we would need to import subroutines anyway.

Show 7 quoted lines
>>> +
>>> +sub __bootstrap_locale_messages {
>>> +     our $TEXTDOMAIN = 'git';
>>> +
>>> +     # TODO: How do I make the sed replacements in the top level
>>> +     # Makefile reach me here?
>>> +     #our $TEXTDOMAINDIR = q|@@LOCALEDIR@@|;
[...]
Show 14 quoted lines
>> Make invoked on perl/Makefile, when invoked from main Makefile by
>> '$(MAKE) -C perl' (via QUIET_SUBDIR0) passes 'localedir' to submake;
>> perl/Makefile should probably have something like
>>
>>  localedir ?= $(sharedir)/locale
>>
>> That is assuming that 'localedir' is added to list of exported
>> variables.
>>
>> But I am not sure how such substitution should be performed.
> 
> I'll try to get something like that to work. Actually the main problem
> seemed to be that it had a hybrid handwritten Makefile and one made by
> EU::MM.

The problem with EU::MM is that while ExtUtils::MakeMaker was first released as a core module with Perl 5, git requires for generated makefile (well, perl.mak included by perl/Makefile) to be compatibile with DESTDIR mechanism. This requires ExtUtils::MakeMaker version 6.11+, which was released with perl 5.006002 (Perl v5.6.2).

Hmmm... perhaps we can bump requirement from Perl 5.6 to Perl 5.6.2 and get rid of hybrid handwritten Makefile?

Otherwise you would have to either duplicate EU::MM work in handwritten Makefile, or somehow make perl/Makefile to do transformation before perl/perl.mak generated by EU::MM starts working...

Show 22 quoted lines
>>> +
>>> +BEGIN
>>> +{
>>> +     local ($@, $!);
>>> +     eval { __bootstrap_locale_messages() };
>>> +     if ($@) {
>>> +             # Oh noes, no Locale::Messages here
>>> +             *gettext = sub ($) { $_[0] };
>>> +     }
>>> +}
>>
>> Does it need to be in BEGIN block?  Probably yes.
> 
> I'm pretty sure it needs to all be at BEGIN time. I recall that
> declaring prototypes at runtime was an error in some older versions,
> but it works now:
> 
>     $ perl -E '*meh = sub ($) { "foo" }; say prototype "meh"'
>     $
> 
> Anyway since it's code that's being use'd it's going to always be at
> BEGIN time anyway. I just wanted to be explicit.
All right, probably better to be safe.
Show 10 quoted lines
>>> +=head1 EXPORTS
>>> +
>>> +Exports are done via L<Exporter>. Invididual functions can be
>>> +exporter, or all of them via the C<:all> export tag.
>>
>> Shouldn't this be described in less technical way in SYNOPSIS and
>> DESCRIPTION sections instead?
> 
> Yeah, maybe. But there's a convention for `=head1 EXPORTS' in perl
> core / CPAN. So that's what I added at a whim.

Hmmm... I don't recall seeing EXPORTS section often in manpages for Perl modules, at least not for the ones I use...

BTW. I tried to find Perl module / Perl program that uses Locale::TextDomain for l10n, but provides passthrough fallback if it is not installed... but haven't find any.

-- 
Jakub Narebski
Poland
Ævar Arnfjörð Bjarmason· May 30, 2010, 20:54 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v2 6/6] gettext: Add a skeleton po/is.po

This example is.po file translates a little bit of C, Shell and Perl as an example.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 po/is.po |  282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 282 insertions(+), 0 deletions(-)
 create mode 100644 po/is.po
Show changes to po/is.po +282 −0
diff --git a/po/is.po b/po/is.po
new file mode 100644
index 0000000..b716e23
--- /dev/null
+++ b/po/is.po
@@ -0,0 +1,282 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Git\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-05-30 19:24+0000\n"
+"PO-Revision-Date: 2010-05-30 19:31+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"
+
+#: wt-status.c:53
+msgid "# Unmerged paths:"
+msgstr ""
+
+#: wt-status.c:59 wt-status.c:76
+#, c-format
+msgid "#   (use \"git reset %s <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:61 wt-status.c:78
+msgid "#   (use \"git rm --cached <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:62
+msgid "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"
+msgstr ""
+
+#: wt-status.c:63 wt-status.c:79 wt-status.c:98 wt-status.c:110
+#: wt-status.c:115 wt-status.c:638 wt-status.c:640
+msgid "#"
+msgstr ""
+
+#: wt-status.c:70
+msgid "# Changes to be committed:"
+msgstr ""
+
+#: wt-status.c:88
+msgid "# Changed but not updated:"
+msgstr ""
+
+#: wt-status.c:92
+msgid "#   (use \"git add <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:94
+msgid "#   (use \"git add/rm <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:95
+msgid ""
+"#   (use \"git checkout -- <file>...\" to discard changes in working "
+"directory)"
+msgstr ""
+
+#: wt-status.c:97
+msgid "#   (commit or discard the untracked or modified content in submodules)"
+msgstr ""
+
+#: wt-status.c:106
+#, c-format
+msgid "# %s files:"
+msgstr ""
+
+#: wt-status.c:109
+#, c-format
+msgid "#   (use \"git %s <file>...\" to include in what will be committed)"
+msgstr ""
+
+#: wt-status.c:126
+msgid "bug"
+msgstr ""
+
+#: wt-status.c:129 wt-status.c:182
+msgid "#\t"
+msgstr ""
+
+#: wt-status.c:131
+msgid "both deleted:"
+msgstr ""
+
+#: wt-status.c:132
+msgid "added by us:"
+msgstr ""
+
+#: wt-status.c:133
+msgid "deleted by them:"
+msgstr ""
+
+#: wt-status.c:134
+msgid "added by them:"
+msgstr ""
+
+#: wt-status.c:135
+msgid "deleted by us:"
+msgstr ""
+
+#: wt-status.c:136
+msgid "both added:"
+msgstr ""
+
+#: wt-status.c:137
+msgid "both modified:"
+msgstr ""
+
+#: wt-status.c:139
+#, c-format
+msgid "%-20s%s\n"
+msgstr ""
+
+#: wt-status.c:165
+msgid " ("
+msgstr ""
+
+#: wt-status.c:167
+msgid "new commits, "
+msgstr ""
+
+#: wt-status.c:169
+msgid "modified content, "
+msgstr ""
+
+#: wt-status.c:171
+msgid "untracked content, "
+msgstr ""
+
+#: wt-status.c:185
+#, c-format
+msgid "new file:   %s"
+msgstr ""
+
+#: wt-status.c:188
+#, c-format
+msgid "copied:     %s -> %s"
+msgstr ""
+
+#: wt-status.c:191
+#, c-format
+msgid "deleted:    %s"
+msgstr ""
+
+#: wt-status.c:194
+#, c-format
+msgid "modified:   %s"
+msgstr ""
+
+#: wt-status.c:197
+#, c-format
+msgid "renamed:    %s -> %s"
+msgstr ""
+
+#: wt-status.c:200
+#, c-format
+msgid "typechange: %s"
+msgstr ""
+
+#: wt-status.c:203
+#, c-format
+msgid "unknown:    %s"
+msgstr ""
+
+#: wt-status.c:206
+#, c-format
+msgid "unmerged:   %s"
+msgstr ""
+
+#: wt-status.c:209
+#, c-format
+msgid "bug: unhandled diff status %c"
+msgstr ""
+
+#: wt-status.c:212
+#, c-format
+msgid "%s"
+msgstr ""
+
+#: wt-status.c:215
+#, c-format
+msgid "\n"
+msgstr ""
+
+#: wt-status.c:622
+msgid "On branch "
+msgstr "Á greininni "
+
+#: wt-status.c:629
+msgid "Not currently on any branch."
+msgstr "Ekki á neinni grein."
+
+#: wt-status.c:639
+msgid "# Initial commit"
+msgstr ""
+
+#: wt-status.c:651
+msgid "Untracked"
+msgstr ""
+
+#: wt-status.c:651
+msgid "add"
+msgstr ""
+
+#: wt-status.c:653
+msgid "Ignored"
+msgstr ""
+
+#: wt-status.c:653
+msgid "add -f"
+msgstr ""
+
+#: wt-status.c:655
+#, c-format
+msgid "# Untracked files not listed%s\n"
+msgstr ""
+
+#: wt-status.c:657
+msgid " (use -u option to show untracked files)"
+msgstr ""
+
+#: wt-status.c:663
+#, c-format
+msgid "# No changes\n"
+msgstr "# Engar breytingar\n"
+
+#: wt-status.c:667
+#, c-format
+msgid "no changes added to commit%s\n"
+msgstr ""
+
+#: wt-status.c:669
+msgid " (use \"git add\" and/or \"git commit -a\")"
+msgstr ""
+
+#: wt-status.c:671
+#, c-format
+msgid "nothing added to commit but untracked files present%s\n"
+msgstr ""
+
+#: wt-status.c:673
+msgid " (use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:676
+msgid " (create/copy files and use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:678 wt-status.c:681
+#, c-format
+msgid "nothing to commit%s\n"
+msgstr ""
+
+#: wt-status.c:679
+msgid " (use -u to show untracked files)"
+msgstr ""
+
+#: wt-status.c:682
+msgid " (working directory clean)"
+msgstr ""
+
+#: git-pull.sh:124
+msgid "Fetching tags only, you probably meant:"
+msgstr ""
+
+#: git-pull.sh:125
+msgid "  git fetch --tags"
+msgstr ""
+
+#: git-pull.sh:157
+msgid ""
+"You are not currently on a branch, so I cannot use any\n"
+"'branch.<branchname>.merge' in your configuration file.\n"
+"Please specify which remote branch you want to use on the command\n"
+"line and try again (e.g. 'git pull <repository> <refspec>').\n"
+"See git-pull(1) for details."
+msgstr ""
+
+#: git-send-email.perl:678
+#, perl-format
+msgid "Emails will be sent from: %s\n"
+msgstr "Póstarnir verða sendir frá: %s\n"
-- 
1.7.1.248.gcd6d1
Jonathan Nieder· May 30, 2010, 21:29 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v2 6/6] gettext: Add a skeleton po/is.po

Hi Ævar,
Ævar Arnfjörð Bjarmason wrote:
> +#: wt-status.c:53
> +msgid "# Unmerged paths:"
> +msgstr ""

On projects that use gettext, merging these line number changes can be a royal pain. In effect, the .po files are mixing semantically meaningful text and automatically generated cruft.

So my ignorant questions:
 . does gettext use the #: comments for anything important?
 . can they be suppressed when generating the .po file?
 . can they be easily re-added if some translation front-end needs
   them?
> +#: git-pull.sh:124
> +msgid "Fetching tags only, you probably meant:"
> +msgstr ""

I noticed there are no printf formats among the shell scripting examples. Would we have to roll our own, like this?

  gettext 'You asked me to pull without telling me which branch you
want to OP_TYPE OP_PREP, and 'branch.CURR_BRANCH.merge' in
your configuration file does not tell me, either. Please
try again (e.g., 'git pull <repository> <refspec>').
See git-pull(1) for details.
' |
  sed "
	s/OP_TYPE/$op_type/g
	s/OP_PREP/$op_prep/g
	s/CURR_BRANCH/$curr_branch/g
  "
Jonathan
Jonathan Nieder· May 30, 2010, 21:39 UTC · re: Jonathan Nieder · lore

Re: [PATCH/RFC v2 6/6] gettext: Add a skeleton po/is.po

Jonathan Nieder wrote:
Show 11 quoted lines
>   gettext 'You asked me to pull without telling me which branch you
> want to OP_TYPE OP_PREP, and 'branch.CURR_BRANCH.merge' in
> your configuration file does not tell me, either. Please
> try again (e.g., 'git pull <repository> <refspec>').
> See git-pull(1) for details.
> ' |
>   sed "
> 	s/OP_TYPE/$op_type/g
> 	s/OP_PREP/$op_prep/g
> 	s/CURR_BRANCH/$curr_branch/g
>   "

Apparently this is what "gettext -e"/eval_gettext is for. Sorry for the noise.

Jonathan
Ævar Arnfjörð Bjarmason· May 31, 2010, 14:17 UTC · re: Jonathan Nieder · lore

Re: [PATCH/RFC v2 6/6] gettext: Add a skeleton po/is.po

On Sun, May 30, 2010 at 21:29, Jonathan Nieder <jrnieder@gmail.com> wrote:
Show 18 quoted lines
> Hi Ęvar,
>
> Ęvar Arnfjörš Bjarmason wrote:
>
>> +#: wt-status.c:53
>> +msgid "# Unmerged paths:"
>> +msgstr ""
>
> On projects that use gettext, merging these line number changes can
> be a royal pain.  In effect, the .po files are mixing semantically
> meaningful text and automatically generated cruft.
>
> So my ignorant questions:
>
>  . does gettext use the #: comments for anything important?
>  . can they be suppressed when generating the .po file?
>  . can they be easily re-added if some translation front-end needs
>   them?
Let me answer that more generally.

Usually when people first see the Gettext format they go "ew!". The thing is that it's really meant as a format for mixed human and programmatic editing. Almost everyone editing PO files doesn't do so purely with a simple non-PO aware text editor.

PO editors use these comments to allow you to jump to the source definition to see the message in context. E.g. Emacs's po-mode allows you to do this, and Launchpad displays the file & line number (but last I checked they didn't have an embedded viewer).

A merge of two PO files that doesn't use msgmerge(1) is going to be painful. There are some things that it'll throw out and regenerate (e.g. automated comments, those matching /^#\S/), and it might move messages around to match their new position in the respective source files. Thus a merge that might be hard with a simple git merge might be very easy with a msgmerge-based merge.

Now, we could filter out all these comments (and e.g. context comments) before checking these files in. But that would cause a lot of tools like Launchpad and PO editors to behave in unexpected ways. The Gettext utilities themselves also seem to have no support for initializing/emitting these sort of slim files.

I don't know what the answer is, but this is all something to consider. I'm new to the Gettext format myself, but I've come to like the automatic editing the tools do, especially the automaticy fuzzy tagging.

Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 0/7] Add internationalization support to Git

Here's version 3 of the patch series begun by Jeff Epler to make Git optionally translatable with GNU gettext.

This version contains a lot of fixes to issues pointe out by Jakub Narebski, and other misc.

Since the last RFC:
  * Squashed some infrastructure patches into the initial commit.
  * Added basic sanity testing for the Gettext support
  * .mo files aren't installed on `make install` time if NO_GETTEXT
    was specified during the build.
    In C gettext support isn't compiled in. But this happens
    implicitly in Shell/Perl because the locale files won't be there
    on the system.
  * NO_GETTEXT is now set correctly depending on whether libintl.h
    exists when autoconf is used to build Git. Whether this is the
    best approach to gettext detection with autoconf is being
    discussed in the "[PATCH/RCF] autoconf: Check if <libintl.h>
    exists and set NO_GETTEX" thread.
  * The localedir is now passed to the Perl code via Makefile
    trickery. It's no longer hardcoded to /usr/local/share/locale
  * Add `--keyword=` to all xgettext invocations. This way we make
    sure that only our gettext functions are extracted, not the
    defaults xgettext.
  * Git::Gettext is now called Git::I18N
  * Remove LICENSE from I18N.pm. It was a boilerplate that shouldn't
    have been there.
  * Use qw() not qw< > in Perl code.
  * Changed the interface to Git::I18N. Now it exports all its
    functions by default. If something's using the module it's going
    to want to import everything anyway. This makes the issue of '@{
    $EXPORT_TAGS{'all'} }' being ugly syntax moot since it isn't used
    anymore.
    Users can still import selectively by specifying arguments to
    import().
  * The GIT_TEXTDOMAINDIR environment variable can be set to override
    where the C/Perl/Shell locale files are found. This is intended to
    be set by the test suite.
  * The boolean test for whether gettext.sh was successfully loaded
    was inverted (oops!)
Things that haven't changed:
  Jakub pointed out some things that I might want to change that I
  didn't yet. Mostly because I didn't have time yet.
  * Git::I18N still uses Locale::Messages, not Locale::TextDomain.
    I'm still not convinced that the low-level Locale::Messages
    interface isn't a better fit for us. But I didn't get
    Locale::TextDomain working at all today so I'm still using
    Locale::Messages.
  * Perl still uses gettext($), not __($) (see above).
  * use Exporter 'import': Subclassing Exporter instead since we
    apparently need Perl 5.6 compatibility.
Jeff Epler (1):
  Add infrastructure for translating Git with gettext
Ævar Arnfjörð Bjarmason (6):
  gettext: Add a Gettext interface for shell scripts
  gettext: Add a Gettext interface for Perl
  Makefile: Don't install Gettext .mo files if NO_GETTEXT
  Makefile: Override --keyword= for all languages
  gettext: Basic sanity tests for Git's Gettext support
  gettext: Add a skeleton po/is.po
 .gitignore          |    1 +
 Makefile            |   43 ++++++++-
 config.mak.in       |    1 +
 configure.ac        |    6 +
 gettext.c           |   25 +++++
 gettext.h           |   13 +++
 git-pull.sh         |   15 ++--
 git-send-email.perl |    3 +-
 git-sh-setup.sh     |   33 ++++++
 git.c               |    3 +
 perl/Git/I18N.pm    |   73 +++++++++++++
 perl/Makefile       |    3 +-
 perl/Makefile.PL    |   14 +++-
 po/.gitignore       |    1 +
 po/is.po            |  282 +++++++++++++++++++++++++++++++++++++++++++++++++++
 t/t0200-gettext.sh  |   28 +++++
 wt-status.c         |  129 ++++++++++++------------
 17 files changed, 597 insertions(+), 76 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 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
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 00:11 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v3 0/7] Add internationalization support to Git

On Tue, Jun 1, 2010 at 23:39, Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:
Show 9 quoted lines
> Here's version 3 of the patch series begun by Jeff Epler to make Git
> optionally translatable with GNU gettext.
>
> This version contains a lot of fixes to issues pointe out by Jakub
> Narebski, and other misc.
> [...]
>  * The GIT_TEXTDOMAINDIR environment variable can be set to override
>    where the C/Perl/Shell locale files are found. This is intended to
>    be set by the test suite.

I made a silly error in my rebases and rewrote this commit out of my tree. Instead of sending new commits here's the `git diff -w` below:

    diff --git a/git-sh-setup.sh b/git-sh-setup.sh
    index d67901c..379c9a1 100644
    --- a/git-sh-setup.sh
    +++ b/git-sh-setup.sh
    @@ -219,7 +219,12 @@ if test $? -eq 0
     then
         TEXTDOMAIN=git
         export TEXDTOMAIN
    +	if [ -z "$GIT_TEXTDOMAINDIR" ]
    +	then
         TEXTDOMAINDIR="@@LOCALEDIR@@"
    +	else
    +		TEXTDOMAINDIR=$GIT_TEXTDOMAINDIR
    +	fi
         export TEXTDOMAINDIR
     else
         # Since GNU gettext.sh isn't available we'll have to define our
    diff --git a/perl/Git/I18N.pm b/perl/Git/I18N.pm
    index 568fd3d..e19d99b 100644
    --- a/perl/Git/I18N.pm
    +++ b/perl/Git/I18N.pm
    @@ -11,7 +11,7 @@ our @EXPORT_OK = @EXPORT;
     sub __bootstrap_locale_messages {
     	our $TEXTDOMAIN = 'git';
    -	our $TEXTDOMAINDIR = '++LOCALEDIR++';
    +	our $TEXTDOMAINDIR = $ENV{GIT_TEXTDOMAINDIR} || '++LOCALEDIR++';
     	require POSIX;
     	POSIX->import(qw(setlocale));
    diff --git a/t/t0200-gettext.sh b/t/t0200-gettext.sh
    index a8f9edd..7c2a4cb 100755
    --- a/t/t0200-gettext.sh
    +++ b/t/t0200-gettext.sh
    @@ -1,5 +1,7 @@
     #!/bin/sh
    +export GIT_TEXTDOMAINDIR=`pwd`/../share/locale
    +
     test_description='Gettext support for Git'
     . ./test-lib.sh
     . ../../git-sh-setup
    @@ -9,7 +11,8 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '
     '
     test_expect_success 'sanity: $TEXTDOMAINDIR exists' '
    -	test -d $TEXTDOMAINDIR
    +	test -d $TEXTDOMAINDIR &&
    +	test $TEXTDOMAINDIR = $GIT_TEXTDOMAINDIR
     '
     test_expect_success 'sanity: Icelandic locale was compiled' '
Along with another silly error:
    diff --git a/git-sh-setup.sh b/git-sh-setup.sh
    index 379c9a1..da8c47f 100644
    --- a/git-sh-setup.sh
    +++ b/git-sh-setup.sh
    @@ -218,7 +218,7 @@ esac
     if test $? -eq 0
     then
         TEXTDOMAIN=git
    -       export TEXDTOMAIN
    +       export TEXTDOMAIN
            if [ -z "$GIT_TEXTDOMAINDIR" ]
            then
                    TEXTDOMAINDIR="@@LOCALEDIR@@"

These fixes will be included (along with any other fixes) in my next series. Which'll be submitted after soliciting more RFC comments. Sorry everyone.

Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:05 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 0/7] Add internationalization support to Git

Here's version 4 of the patch series begun by Jeff Epler to make Git optionally translatable with GNU gettext.

It's like the version 4 I submitted an ~hour ago, sans the annoying errors I made and lots of tests:

Changes from v3 to v4:
  * Fix typos and removal of GIT_TEXTDOMAINDIR that I rewrote out with
    some rebase mistakes.
  * Add meaningful testing. The C, Shell and Perl wrappers are now
    tested as part of a new t/t0200-gettext.sh test file. Put under
    the t/t0* namespace because it's a core feature, and using a new
    t02 prefix because there'll probably be a lot of Gettext tests
    down the line.
Changes from v2 to v3:
  * Squashed some infrastructure patches into the initial commit.
  * Added basic sanity testing for the Gettext support
  * .mo files aren't installed on `make install` time if NO_GETTEXT
    was specified during the build.
    In C gettext support isn't compiled in. But this happens
    implicitly in Shell/Perl because the locale files won't be there
    on the system.
  * NO_GETTEXT is now set correctly depending on whether libintl.h
    exists when autoconf is used to build Git. Whether this is the
    best approach to gettext detection with autoconf is being
    discussed in the "[PATCH/RCF] autoconf: Check if <libintl.h>
    exists and set NO_GETTEX" thread.
  * The localedir is now passed to the Perl code via Makefile
    trickery. It's no longer hardcoded to /usr/local/share/locale
  * Add `--keyword=` to all xgettext invocations. This way we make
    sure that only our gettext functions are extracted, not the
    defaults xgettext.
  * Git::Gettext is now called Git::I18N
  * Remove LICENSE from I18N.pm. It was a boilerplate that shouldn't
    have been there.
  * Use qw() not qw< > in Perl code.
  * Changed the interface to Git::I18N. Now it exports all its
    functions by default. If something's using the module it's going
    to want to import everything anyway. This makes the issue of '@{
    $EXPORT_TAGS{'all'} }' being ugly syntax moot since it isn't used
    anymore.
    Users can still import selectively by specifying arguments to
    import().
  * The GIT_TEXTDOMAINDIR environment variable can be set to override
    where the C/Perl/Shell locale files are found. This is intended to
    be set by the test suite.
  * The boolean test for whether gettext.sh was successfully loaded
    was inverted (oops!)
Things that haven't changed:
  Jakub pointed out some things that I might want to change that I
  didn't yet. Mostly because I didn't have time yet.
  * Git::I18N still uses Locale::Messages, not Locale::TextDomain.
    I'm still not convinced that the low-level Locale::Messages
    interface isn't a better fit for us. But I didn't get
    Locale::TextDomain working at all today so I'm still using
    Locale::Messages.
  * Perl still uses gettext($), not __($) (see above).
  * use Exporter 'import': Subclassing Exporter instead since we
    apparently need Perl 5.6 compatibility.
Jeff Epler (1):
  Add infrastructure for translating Git with gettext
Ævar Arnfjörð Bjarmason (6):
  gettext: Add a Gettext interface for shell scripts
  gettext: Add a Gettext interface for Perl
  Makefile: Don't install Gettext .mo files if NO_GETTEXT
  Makefile: Override --keyword= for all languages
  gettext: Sanity tests for Git's Gettext support
  gettext: Add a skeleton po/is.po
 .gitignore          |    1 +
 Makefile            |   43 ++++++++-
 config.mak.in       |    1 +
 configure.ac        |    6 +
 gettext.c           |   25 +++++
 gettext.h           |   13 +++
 git-pull.sh         |   15 ++--
 git-send-email.perl |    3 +-
 git-sh-setup.sh     |   38 +++++++
 git.c               |    3 +
 perl/Git/I18N.pm    |   73 +++++++++++++
 perl/Makefile       |    3 +-
 perl/Makefile.PL    |   14 +++-
 po/.gitignore       |    1 +
 po/is.po            |  282 +++++++++++++++++++++++++++++++++++++++++++++++++++
 t/t0200-gettext.sh  |   73 +++++++++++++
 wt-status.c         |  129 ++++++++++++------------
 17 files changed, 647 insertions(+), 76 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 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
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:05 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 1/7] Add infrastructure for translating Git with gettext

From: Jeff Epler <jepler@unpythonic.net>

Change the build process to use GNU's libintl if it's available. If not we define our own skeleton replacement functions which degrade gracefully to English.

Gettext .mo files will be installed and looked for in $(prefix)/share/locale. GIT_LOCALEDIR can also be set to override that, this variable is only intended for testing Git itself.

Signed-off-by: Jeff Epler <jepler@unpythonic.net>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore    |    1 +
 Makefile      |   30 +++++++++++++
 config.mak.in |    1 +
 configure.ac  |    6 +++
 gettext.c     |   25 +++++++++++
 gettext.h     |   13 ++++++
 git.c         |    3 +
 po/.gitignore |    1 +
 wt-status.c   |  129 +++++++++++++++++++++++++++++----------------------------
 9 files changed, 145 insertions(+), 64 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 create mode 100644 po/.gitignore
Show changes to 9 files +145 −64

.gitignore, Makefile, config.mak.in, configure.ac, gettext.c, gettext.h, git.c, po/.gitignore, wt-status.c

diff --git a/.gitignore b/.gitignore
index 14e2b6b..d22c0e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -204,3 +204,4 @@
 *.pdb
 /Debug/
 /Release/
+/share/
diff --git a/Makefile b/Makefile
index d5d6565..4de0627 100644
--- a/Makefile
+++ b/Makefile
@@ -297,6 +297,8 @@ RPMBUILD = rpmbuild
 TCL_PATH = tclsh
 TCLTK_PATH = wish
 PTHREAD_LIBS = -lpthread
+XGETTEXT = xgettext
+MSGFMT = msgfmt
 
 export TCL_PATH TCLTK_PATH
 
@@ -523,6 +525,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 +567,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 +1390,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 +1425,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,6 +1453,7 @@ gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
 template_dir_SQ = $(subst ','\'',$(template_dir))
 htmldir_SQ = $(subst ','\'',$(htmldir))
 prefix_SQ = $(subst ','\'',$(prefix))
+sharedir_SQ = $(subst ','\'',$(sharedir))
 
 SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
@@ -1868,6 +1880,17 @@ cscope:
 	$(RM) cscope*
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
+pot:
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+
+POFILES := $(wildcard po/*.po)
+MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
+MODIRS := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/,$(POFILES))
+all:: $(MOFILES)
+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 +2003,9 @@ 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)'
+	$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(sharedir_SQ)/locale'
+	(cd share && tar cf - locale) | \
+		(cd '$(DESTDIR_SQ)$(sharedir_SQ)' && umask 022 && tar xof -)
 	$(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install
 ifndef NO_PERL
 	$(MAKE) -C perl prefix='$(prefix_SQ)' DESTDIR='$(DESTDIR_SQ)' install
@@ -2127,6 +2153,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.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/po/.gitignore b/po/.gitignore
new file mode 100644
index 0000000..221000e
--- /dev/null
+++ b/po/.gitignore
@@ -0,0 +1 @@
+/*.pot
diff --git a/wt-status.c b/wt-status.c
index 14e0acc..70b4293 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -9,6 +9,7 @@
 #include "quote.h"
 #include "run-command.h"
 #include "remote.h"
+#include "gettext.h"
 
 static char default_wt_status_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_NORMAL, /* WT_STATUS_HEADER */
@@ -49,33 +50,33 @@ static void wt_status_print_unmerged_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Unmerged paths:");
+	color_fprintf_ln(s->fp, c, _("# Unmerged paths:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		;
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_cached_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changes to be committed:");
+	color_fprintf_ln(s->fp, c, _("# Changes to be committed:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		; /* NEEDSWORK: use "git reset --unresolve"??? */
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_dirty_header(struct wt_status *s,
@@ -84,17 +85,17 @@ static void wt_status_print_dirty_header(struct wt_status *s,
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changed but not updated:");
+	color_fprintf_ln(s->fp, c, _("# Changed but not updated:"));
 	if (!advice_status_hints)
 		return;
 	if (!has_deleted)
-		color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to update what will be committed)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add <file>...\" to update what will be committed)"));
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" to update what will be committed)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git checkout -- <file>...\" to discard changes in working directory)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" to update what will be committed)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git checkout -- <file>...\" to discard changes in working directory)"));
 	if (has_dirty_submodules)
-		color_fprintf_ln(s->fp, c, "#   (commit or discard the untracked or modified content in submodules)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (commit or discard the untracked or modified content in submodules)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_other_header(struct wt_status *s,
@@ -102,16 +103,16 @@ static void wt_status_print_other_header(struct wt_status *s,
 					 const char *how)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
-	color_fprintf_ln(s->fp, c, "# %s files:", what);
+	color_fprintf_ln(s->fp, c, _("# %s files:"), what);
 	if (!advice_status_hints)
 		return;
-	color_fprintf_ln(s->fp, c, "#   (use \"git %s <file>...\" to include in what will be committed)", how);
-	color_fprintf_ln(s->fp, c, "#");
+	color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to include in what will be committed)"), how);
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_trailer(struct wt_status *s)
 {
-	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 }
 
 #define quote_path quote_path_relative
@@ -122,20 +123,20 @@ static void wt_status_print_unmerged_data(struct wt_status *s,
 	const char *c = color(WT_STATUS_UNMERGED, s);
 	struct wt_status_change_data *d = it->util;
 	struct strbuf onebuf = STRBUF_INIT;
-	const char *one, *how = "bug";
+	const char *one, *how = _("bug");
 
 	one = quote_path(it->string, -1, &onebuf, s->prefix);
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (d->stagemask) {
-	case 1: how = "both deleted:"; break;
-	case 2: how = "added by us:"; break;
-	case 3: how = "deleted by them:"; break;
-	case 4: how = "added by them:"; break;
-	case 5: how = "deleted by us:"; break;
-	case 6: how = "both added:"; break;
-	case 7: how = "both modified:"; break;
+	case 1: how = _("both deleted:"); break;
+	case 2: how = _("added by us:"); break;
+	case 3: how = _("deleted by them:"); break;
+	case 4: how = _("added by them:"); break;
+	case 5: how = _("deleted by us:"); break;
+	case 6: how = _("both added:"); break;
+	case 7: how = _("both modified:"); break;
 	}
-	color_fprintf(s->fp, c, "%-20s%s\n", how, one);
+	color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);
 	strbuf_release(&onebuf);
 }
 
@@ -161,13 +162,13 @@ static void wt_status_print_change_data(struct wt_status *s,
 		break;
 	case WT_STATUS_CHANGED:
 		if (d->new_submodule_commits || d->dirty_submodule) {
-			strbuf_addstr(&extra, " (");
+			strbuf_addstr(&extra, _(" ("));
 			if (d->new_submodule_commits)
-				strbuf_addf(&extra, "new commits, ");
+				strbuf_addf(&extra, _("new commits, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
-				strbuf_addf(&extra, "modified content, ");
+				strbuf_addf(&extra, _("modified content, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_UNTRACKED)
-				strbuf_addf(&extra, "untracked content, ");
+				strbuf_addf(&extra, _("untracked content, "));
 			strbuf_setlen(&extra, extra.len - 2);
 			strbuf_addch(&extra, ')');
 		}
@@ -178,40 +179,40 @@ static void wt_status_print_change_data(struct wt_status *s,
 	one = quote_path(one_name, -1, &onebuf, s->prefix);
 	two = quote_path(two_name, -1, &twobuf, s->prefix);
 
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (status) {
 	case DIFF_STATUS_ADDED:
-		color_fprintf(s->fp, c, "new file:   %s", one);
+		color_fprintf(s->fp, c, _("new file:   %s"), one);
 		break;
 	case DIFF_STATUS_COPIED:
-		color_fprintf(s->fp, c, "copied:     %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("copied:     %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_DELETED:
-		color_fprintf(s->fp, c, "deleted:    %s", one);
+		color_fprintf(s->fp, c, _("deleted:    %s"), one);
 		break;
 	case DIFF_STATUS_MODIFIED:
-		color_fprintf(s->fp, c, "modified:   %s", one);
+		color_fprintf(s->fp, c, _("modified:   %s"), one);
 		break;
 	case DIFF_STATUS_RENAMED:
-		color_fprintf(s->fp, c, "renamed:    %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("renamed:    %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_TYPE_CHANGED:
-		color_fprintf(s->fp, c, "typechange: %s", one);
+		color_fprintf(s->fp, c, _("typechange: %s"), one);
 		break;
 	case DIFF_STATUS_UNKNOWN:
-		color_fprintf(s->fp, c, "unknown:    %s", one);
+		color_fprintf(s->fp, c, _("unknown:    %s"), one);
 		break;
 	case DIFF_STATUS_UNMERGED:
-		color_fprintf(s->fp, c, "unmerged:   %s", one);
+		color_fprintf(s->fp, c, _("unmerged:   %s"), one);
 		break;
 	default:
-		die("bug: unhandled diff status %c", status);
+		die(_("bug: unhandled diff status %c"), status);
 	}
 	if (extra.len) {
-		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s", extra.buf);
+		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("%s"), extra.buf);
 		strbuf_release(&extra);
 	}
-	fprintf(s->fp, "\n");
+	fprintf(s->fp, _("\n"));
 	strbuf_release(&onebuf);
 	strbuf_release(&twobuf);
 }
@@ -618,14 +619,14 @@ void wt_status_print(struct wt_status *s)
 	const char *branch_color = color(WT_STATUS_HEADER, s);
 
 	if (s->branch) {
-		const char *on_what = "On branch ";
+		const char *on_what = _("On branch ");
 		const char *branch_name = s->branch;
 		if (!prefixcmp(branch_name, "refs/heads/"))
 			branch_name += 11;
 		else if (!strcmp(branch_name, "HEAD")) {
 			branch_name = "";
 			branch_color = color(WT_STATUS_NOBRANCH, s);
-			on_what = "Not currently on any branch.";
+			on_what = _("Not currently on any branch.");
 		}
 		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "# ");
 		color_fprintf_ln(s->fp, branch_color, "%s%s", on_what, branch_name);
@@ -634,9 +635,9 @@ void wt_status_print(struct wt_status *s)
 	}
 
 	if (s->is_initial) {
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "# Initial commit");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("# Initial commit"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 	}
 
 	wt_status_print_updated(s);
@@ -647,38 +648,38 @@ void wt_status_print(struct wt_status *s)
 		wt_status_print_submodule_summary(s, 1);  /* unstaged */
 	}
 	if (s->show_untracked_files) {
-		wt_status_print_other(s, &s->untracked, "Untracked", "add");
+		wt_status_print_other(s, &s->untracked, _("Untracked"), _("add"));
 		if (s->show_ignored_files)
-			wt_status_print_other(s, &s->ignored, "Ignored", "add -f");
+			wt_status_print_other(s, &s->ignored, _("Ignored"), _("add -f"));
 	} else if (s->commitable)
-		fprintf(s->fp, "# Untracked files not listed%s\n",
+		fprintf(s->fp, _("# Untracked files not listed%s\n"),
 			advice_status_hints
-			? " (use -u option to show untracked files)" : "");
+			? _(" (use -u option to show untracked files)") : "");
 
 	if (s->verbose)
 		wt_status_print_verbose(s);
 	if (!s->commitable) {
 		if (s->amend)
-			fprintf(s->fp, "# No changes\n");
+			fprintf(s->fp, _("# No changes\n"));
 		else if (s->nowarn)
 			; /* nothing */
 		else if (s->workdir_dirty)
-			printf("no changes added to commit%s\n",
+			printf(_("no changes added to commit%s\n"),
 				advice_status_hints
-				? " (use \"git add\" and/or \"git commit -a\")" : "");
+				? _(" (use \"git add\" and/or \"git commit -a\")") : "");
 		else if (s->untracked.nr)
-			printf("nothing added to commit but untracked files present%s\n",
+			printf(_("nothing added to commit but untracked files present%s\n"),
 				advice_status_hints
-				? " (use \"git add\" to track)" : "");
+				? _(" (use \"git add\" to track)") : "");
 		else if (s->is_initial)
 			printf("nothing to commit%s\n", advice_status_hints
-				? " (create/copy files and use \"git add\" to track)" : "");
+				? _(" (create/copy files and use \"git add\" to track)") : "");
 		else if (!s->show_untracked_files)
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (use -u to show untracked files)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (use -u to show untracked files)") : "");
 		else
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (working directory clean)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (working directory clean)") : "");
 	}
 }
 
-- 
1.7.1.251.gec7f5.dirty
Peter Krefting· Jun 2, 2010, 09:12 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v4 1/7] Add infrastructure for translating Git with gettext

Ævar Arnfjörð Bjarmason:
> +		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
> +	color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
> +	color_fprintf_ln(s->fp, c, _("#"));
This is a multi-line string, and should be translated as such.

The trailing "#" should either be part of the translated multi-line string, or be added explicitely, and untranslated, at the end.

> +		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
> +	color_fprintf_ln(s->fp, c, _("#"));
Determine how to handle the trailing "#" as above.
> +		color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" to update what will be committed)"));
> +	color_fprintf_ln(s->fp, c, _("#   (use \"git checkout -- <file>...\" to discard changes in working directory)"));
Multi-line.
> +		color_fprintf_ln(s->fp, c, _("#   (commit or discard the untracked or modified content in submodules)"));
> +	color_fprintf_ln(s->fp, c, _("#"));
Trailing "#".
> +	color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to include in what will be committed)"), how);
> +	color_fprintf_ln(s->fp, c, _("#"));
Ditto.
Show 10 quoted lines
> +	case 1: how = _("both deleted:"); break;
> +	case 2: how = _("added by us:"); break;
> +	case 3: how = _("deleted by them:"); break;
> +	case 4: how = _("added by them:"); break;
> +	case 5: how = _("deleted by us:"); break;
> +	case 6: how = _("both added:"); break;
> +	case 7: how = _("both modified:"); break;
> 	}
> -	color_fprintf(s->fp, c, "%-20s%s\n", how, one);
> +	color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);

does color_fprintf() gracefully handle cutting text for multi-byte encodings, and text width? Otherwise I can see this bite us for all non-ASCII locales in general (if long translated are needed), and for CJK locales in particular (for variable character widths).

> -		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s", extra.buf);
> +		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("%s"), extra.buf);
Should not be in _()
> -	fprintf(s->fp, "\n");
> +	fprintf(s->fp, _("\n"));
Ditto.
> +		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
> +		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("# Initial commit"));
> +		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
Either a multi-line string, or untranslate the "#"s.
-- 
\\// Peter - http://www.softwolves.pp.se/
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 09:29 UTC · re: Peter Krefting · lore

Re: [PATCH/RFC v4 1/7] Add infrastructure for translating Git with gettext

On Wed, Jun 2, 2010 at 09:12, Peter Krefting <peter@softwolves.pp.se> wrote:
Show 74 quoted lines
> Ævar Arnfjörð Bjarmason:
>
>> +               color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached
>> <file>...\" to unstage)"));
>> +       color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as
>> appropriate to mark resolution)"));
>> +       color_fprintf_ln(s->fp, c, _("#"));
>
> This is a multi-line string, and should be translated as such.
>
> The trailing "#" should either be part of the translated multi-line string,
> or be added explicitely, and untranslated, at the end.
>
>> +               color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached
>> <file>...\" to unstage)"));
>> +       color_fprintf_ln(s->fp, c, _("#"));
>
> Determine how to handle the trailing "#" as above.
>
>> +               color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm
>> <file>...\" to update what will be committed)"));
>> +       color_fprintf_ln(s->fp, c, _("#   (use \"git checkout --
>> <file>...\" to discard changes in working directory)"));
>
> Multi-line.
>
>> +               color_fprintf_ln(s->fp, c, _("#   (commit or discard the
>> untracked or modified content in submodules)"));
>> +       color_fprintf_ln(s->fp, c, _("#"));
>
> Trailing "#".
>
>> +       color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to
>> include in what will be committed)"), how);
>> +       color_fprintf_ln(s->fp, c, _("#"));
>
> Ditto.
>
>> +       case 1: how = _("both deleted:"); break;
>> +       case 2: how = _("added by us:"); break;
>> +       case 3: how = _("deleted by them:"); break;
>> +       case 4: how = _("added by them:"); break;
>> +       case 5: how = _("deleted by us:"); break;
>> +       case 6: how = _("both added:"); break;
>> +       case 7: how = _("both modified:"); break;
>>        }
>> -       color_fprintf(s->fp, c, "%-20s%s\n", how, one);
>> +       color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);
>
> does color_fprintf() gracefully handle cutting text for multi-byte
> encodings, and text width? Otherwise I can see this bite us for all
> non-ASCII locales in general (if long translated are needed), and for CJK
> locales in particular (for variable character widths).
>
>> -               color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s",
>> extra.buf);
>> +               color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("%s"),
>> extra.buf);
>
> Should not be in _()
>
>> -       fprintf(s->fp, "\n");
>> +       fprintf(s->fp, _("\n"));
>
> Ditto.
>
>> +               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s),
>> _("#"));
>> +               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#
>> Initial commit"));
>> +               color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s),
>> _("#"));
>
> Either a multi-line string, or untranslate the "#"s.

All of the points you raised are correct. This is a bad way to do things.

It was added in Jeff in his original WIP patch as an example of translation being possible, but I should just remove it. It's not ready to go in for the reasons you cited.

I'll just remove the wt-status.c chunk out of the patch completely in the next submission, and instead translate a few select strings in some C program well.

Making things like wt-status.c translatable is going to be hard to do properly. It uses a lot of lego (sticking strings incrementally together), and due to the coloring of output this'll be particularly hard to convert.

(Also, for reference, here's a general guide on how to translate C programs well):

    http://gnu.april.org/software/automake/manual/gettext/Preparing-Strings.html
Peter Krefting· Jun 2, 2010, 10:11 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v4 1/7] Add infrastructure for translating Git with gettext

Ævar Arnfjörð Bjarmason:
> Making things like wt-status.c translatable is going to be hard to do 
> properly. It uses a lot of lego (sticking strings incrementally together),

However, preparing them for translation is probably going to make them better anyway, as this kind of string building is incredibly brittle, even for English strings.

You also have to look out for pluralisation issues, fortunately Gettext does have support for the various types of plural forms that are used, so it should be possible. The problem is just identifying where they are needed.

> and due to the coloring of output this'll be particularly hard to convert.

Is the colouring based on knowing the contents of the actual text, or is it enough to know the markers? I admit not having looked at the colouring code at all...

-- 
\\// Peter - http://www.softwolves.pp.se/
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 10:56 UTC · re: Peter Krefting · lore

Re: [PATCH/RFC v4 1/7] Add infrastructure for translating Git with gettext

On Wed, Jun 2, 2010 at 10:11, Peter Krefting <peter@softwolves.pp.se> wrote:
Show 8 quoted lines
> Ævar Arnfjörð Bjarmason:
>
>> Making things like wt-status.c translatable is going to be hard to do
>> properly. It uses a lot of lego (sticking strings incrementally together),
>
> However, preparing them for translation is probably going to make them
> better anyway, as this kind of string building is incredibly brittle, even
> for English strings.

Yeah, but they aren't better as they are now. Personally I'd rather incrementally add translation support correctly than do an incomplete job of it and wait for the complaints (and subsequent fixes) to come in.

But maybe that's counter-productive. Just making the English strings translatable as-is is still going to make most of Git translatable, and it isn't going to be that bad for languages like German and French.

Maybe worse is better :)
> You also have to look out for pluralisation issues, fortunately Gettext does
> have support for the various types of plural forms that are used, so it
> should be possible. The problem is just identifying where they are needed.

Yeah. I'm going to support pluralisation (and maybe msgctxt) across the C-Perl-Shell stack when there's a need for it. I haven't found any message in Git yet that requires it, but then again I haven't looked hard either.

It would be useful to have an example of a message that could use pluralization, even better examples in all of C, Perl and Shell.

Show 5 quoted lines
>> and due to the coloring of output this'll be particularly hard to convert.
>
> Is the colouring based on knowing the contents of the actual text, or is it
> enough to know the markers? I admit not having looked at the colouring code
> at all...

There's nothing wrong with coloring in particular, it's just that due to the nature of the current implementation code that does coloring ends up being a bad offender in the "assemble the strings a few bytes at a time with strcat()" department.

That can be fixed, it just takes a bit of time and tedium.
Peter Krefting· Jun 2, 2010, 11:31 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v4 1/7] Add infrastructure for translating Git with gettext

Ævar Arnfjörð Bjarmason:
> But maybe that's counter-productive. Just making the English strings 
> translatable as-is is still going to make most of Git translatable, and it 
> isn't going to be that bad for languages like German and French.

I think this is the best approach. Without actually making *everything* available for translation it is impossible to weed out the problems. Some of the easier "hack" strings (plural word%s, sentence %s%s building, etc) can be fixed during conversion, the rest can be done later as complai^Wcomments come in.

> It would be useful to have an example of a message that could use 
> pluralization, even better examples in all of C, Perl and Shell.

There is the "Your branch is ahead of..." messages, they were introduced in commit b0030db331141bedfaf02f34a83f18712c0ae011, and are currently using a "commit%s" form, where the %s either maps to an empty string or "s". Should be fairly trivial to fix. I think there are other examples as well, but that's the first one I came to think of. :-)

-- 
\\// Peter - http://www.softwolves.pp.se/
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:05 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 2/7] gettext: Add a Gettext interface for shell scripts

Use GNU's gettext.sh in git-sh-setup if it's available, otherwise fallback on our own custom functions.

A couple of strings in git-pull.sh are now translatable as a proof of concept, including a multiline string.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile        |    7 +++++--
 git-pull.sh     |   15 ++++++++-------
 git-sh-setup.sh |   38 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 51 insertions(+), 9 deletions(-)
Show changes to 3 files +51 −9

Makefile, git-pull.sh, git-sh-setup.sh

diff --git a/Makefile b/Makefile
index 4de0627..dce2faa 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 localedir sysconfdir
 
 CC = gcc
 AR = ar
@@ -1455,6 +1456,7 @@ 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))
@@ -1548,6 +1550,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 >$@+
@@ -1881,7 +1884,7 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-pull.sh b/git-pull.sh
index 1a4729f..22a6da2 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -121,8 +121,8 @@ error_on_no_merge_candidates () {
 	do
 		case "$opt" in
 		-t|--t|--ta|--tag|--tags)
-			echo "Fetching tags only, you probably meant:"
-			echo "  git fetch --tags"
+			gettext "Fetching tags only, you probably meant:"; echo
+			gettext "  git fetch --tags"; echo
 			exit 1
 		esac
 	done
@@ -154,11 +154,12 @@ error_on_no_merge_candidates () {
 		echo "a branch. Because this is not the default configured remote"
 		echo "for your current branch, you must specify a branch on the command line."
 	elif [ -z "$curr_branch" ]; then
-		echo "You are not currently on a branch, so I cannot use any"
-		echo "'branch.<branchname>.merge' in your configuration file."
-		echo "Please specify which remote branch you want to use on the command"
-		echo "line and try again (e.g. 'git pull <repository> <refspec>')."
-		echo "See git-pull(1) for details."
+        gettext "You are not currently on a branch, so I cannot use any
+'branch.<branchname>.merge' in your configuration file.
+Please specify which remote branch you want to use on the command
+line and try again (e.g. 'git pull <repository> <refspec>').
+See git-pull(1) for details.";
+        echo
 	elif [ -z "$upstream" ]; then
 		echo "You asked me to pull without telling me which branch you"
 		echo "want to $op_type $op_prep, and 'branch.${curr_branch}.merge' in"
diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 6131670..53cfc42 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -211,3 +211,41 @@ case $(uname -s) in
 	}
 	;;
 esac
+
+# Try to use libintl's gettext.sh, or fall back to English if we
+# can't.
+. gettext.sh
+if test $? -eq 0
+then
+	TEXTDOMAIN=git
+	export TEXTDOMAIN
+	if [ -z "$GIT_TEXTDOMAINDIR" ]
+	then
+		TEXTDOMAINDIR="@@LOCALEDIR@@"
+	else
+		TEXTDOMAINDIR=$GIT_TEXTDOMAINDIR
+	fi
+	export TEXTDOMAINDIR
+else
+	# Since GNU gettext.sh isn't available we'll have to define our
+	# own dummy functions.
+
+	# This code adapted from NessusClient-1.0.2's nessusclient-mkcert
+	# by Michel Arboi <arboi@alussinan.org>. The original code is
+	# under the GPLv2.
+
+	# Not everyone has echo -n
+	case $(echo -n) in
+		\-n)	Xn=	  ; Xc='\c' ;;
+		*)		Xn=-n ; Xc=
+	esac
+
+	gettext () {
+		echo $Xn "$1" $Xc
+	}
+
+	eval_gettext () {
+		eval_gettext_var="echo $1"
+		echo $Xn `eval $eval_gettext_var` $Xc
+	}
+fi
-- 
1.7.1.251.gec7f5.dirty
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:06 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 3/7] gettext: Add a Gettext interface for Perl

Make Git's gettext messages available to Perl programs through Locale::Messages. Gracefully fall back to English on systems that don't contain the module.

This contains some makefile hacks to pass localedir to perl/Makefile. Which in turn passes it to perl.mak, which'll use it to search/replace occurances of ++LOCALEDIR++ in .pm files.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile            |    6 +++-
 git-send-email.perl |    3 +-
 perl/Git/I18N.pm    |   73 +++++++++++++++++++++++++++++++++++++++++++++++++++
 perl/Makefile       |    3 +-
 perl/Makefile.PL    |   14 +++++++++-
 5 files changed, 94 insertions(+), 5 deletions(-)
 create mode 100644 perl/Git/I18N.pm
Show changes to 5 files +94 −5

Makefile, git-send-email.perl, perl/Git/I18N.pm, perl/Makefile, perl/Makefile.PL

diff --git a/Makefile b/Makefile
index dce2faa..3f0e305 100644
--- a/Makefile
+++ b/Makefile
@@ -1505,7 +1505,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
@@ -1884,7 +1884,9 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
+	$(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
+	$(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
+	$(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-send-email.perl b/git-send-email.perl
index 111c981..b9288af 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -26,6 +26,7 @@ use Term::ANSIColor;
 use File::Temp qw/ tempdir tempfile /;
 use Error qw(:try);
 use Git;
+use Git::I18N;
 
 Getopt::Long::Configure qw/ pass_through /;
 
@@ -674,7 +675,7 @@ if (!defined $sender) {
 	$sender = $repoauthor || $repocommitter || '';
 	$sender = ask("Who should the emails appear to be from? [$sender] ",
 	              default => $sender);
-	print "Emails will be sent from: ", $sender, "\n";
+	printf gettext("Emails will be sent from: %s\n"), $sender;
 	$prompting++;
 }
 
diff --git a/perl/Git/I18N.pm b/perl/Git/I18N.pm
new file mode 100644
index 0000000..e19d99b
--- /dev/null
+++ b/perl/Git/I18N.pm
@@ -0,0 +1,73 @@
+package Git::I18N;
+use strict;
+use warnings;
+use Exporter;
+use base 'Exporter';
+
+our $VERSION = '0.01';
+
+our @EXPORT = qw(gettext);
+our @EXPORT_OK = @EXPORT;
+
+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
+		*gettext = sub ($) { $_[0] };
+	}
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Git::I18N - Perl interface to Git's Gettext localizations
+
+=head1 SYNOPSIS
+
+	use Git::I18N;
+
+	print gettext("Welcome to Git!\n");
+
+	printf gettext("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 gettext($)
+
+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'
 );
-- 
1.7.1.251.gec7f5.dirty
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:06 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 4/7] Makefile: Don't install Gettext .mo files if NO_GETTEXT

Change the Makefile not to build/install .mo files as part of the default target if NO_GETTEXT=1 is given.

This was both redundant, and meant that the Perl and Shell programs would use the Gettext strings, since they don't use the git_setup_gettext() function in gettext.c, which is compiled to a stub if NO_GETTEXT=1 is set.

Reported-by: Jakub Narebski <jnareb@gmail.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)
Show changes to Makefile +4 −0
diff --git a/Makefile b/Makefile
index 3f0e305..40eb1a2 100644
--- a/Makefile
+++ b/Makefile
@@ -1891,7 +1891,9 @@ pot:
 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 $@ $<
@@ -2008,9 +2010,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
-- 
1.7.1.251.gec7f5.dirty
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:06 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 5/7] Makefile: Override --keyword= for all languages

By default xgettext will supply a default list of keywords depending on the --language parameter. Disable this in favor of supplying a list of keywords manually.

This reduced the surface area for potential bugs.
---
 Makefile |    6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)
Show changes to Makefile +3 −3
diff --git a/Makefile b/Makefile
index 40eb1a2..62b8279 100644
--- a/Makefile
+++ b/Makefile
@@ -1884,9 +1884,9 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
-	$(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
-	$(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
+	$(XGETTEXT) --keyword= --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
+	$(XGETTEXT) --join-existing --keyword= --keyword=gettext --output=po/git.pot --language=Shell $(SCRIPT_SH)
+	$(XGETTEXT) --join-existing --keyword= --keyword=gettext --output=po/git.pot --language=Perl $(SCRIPT_PERL)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
-- 
1.7.1.251.gec7f5.dirty
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:06 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 6/7] gettext: Sanity tests for Git's Gettext support

These tests check if Git's C, Shell and Perl wrappers for Gettext are
working.
---
 t/t0200-gettext.sh |   73 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 73 insertions(+), 0 deletions(-)
 create mode 100755 t/t0200-gettext.sh
Show changes to t/t0200-gettext.sh +73 −0
diff --git a/t/t0200-gettext.sh b/t/t0200-gettext.sh
new file mode 100755
index 0000000..ff2f9f9
--- /dev/null
+++ b/t/t0200-gettext.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+D=`pwd`
+export GIT_TEXTDOMAINDIR=$D/../share/locale
+
+test_description='Gettext support for Git'
+. ./test-lib.sh
+. ../../git-sh-setup
+
+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: gettext("") metadata is OK' '
+    LC_ALL=is_IS.UTF-8 gettext "" > expect &&
+    grep "Project-Id-Version: Git" expect &&
+    grep "Git Mailing List <git@vger.kernel.org>" expect &&
+    grep "Content-Type: text/plain; charset=UTF-8" expect &&
+    grep "Content-Transfer-Encoding: 8bit" 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 &&
+    test_cmp expect actual
+'
+
+test_expect_success 'sanity: C program git-status reads our message catalog ' '
+    > foo &&
+    test_commit foo "a message"
+    git checkout -b topic/gettext-testing &&
+    git status | grep topic/gettext-testing > expect
+    echo "# On branch topic/gettext-testing" > actual
+    test_cmp expect actual &&
+    LC_ALL=is_IS.UTF-8 git status | grep topic/gettext-testing > expect
+    echo "# Á greininni topic/gettext-testing" > actual
+    test_cmp expect actual
+'
+
+test_expect_success 'sanity: Perl program Git::I18N reads our message catalog ' '
+    echo "On branch " > expect &&
+    perl -I"$D/../perl" -MGit::I18N -le "print gettext(q[On branch ])" > actual &&
+    test_cmp expect actual &&
+
+    echo "Á greininni " > expect &&
+    LC_ALL=is_IS.UTF-8 perl -I"$D/../perl" -MGit::I18N -le "print gettext(q[On branch ])" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'Setup another Git repository for testing' '
+    mkdir parent &&
+    (cd parent && git init &&
+     echo one >file && git add file &&
+     git commit -m one)
+'
+
+test_expect_success 'sanity: Shell program git-pull reads our message catalog' '
+    cd parent &&
+    (git pull --tags "../" >out 2>err || :) &&
+    grep "Fetching tags only" err &&
+    (LC_ALL=is_IS.UTF-8 git pull --tags "../" >out 2>err || :) &&
+    grep "Næ aðeins í" err 
+'
+
+test_done
-- 
1.7.1.251.gec7f5.dirty
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 01:06 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v4 7/7] gettext: Add a skeleton po/is.po

This example is.po file translates a little bit of C, Shell and Perl as an example.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 po/is.po |  282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 282 insertions(+), 0 deletions(-)
 create mode 100644 po/is.po
Show changes to po/is.po +282 −0
diff --git a/po/is.po b/po/is.po
new file mode 100644
index 0000000..aa1923e
--- /dev/null
+++ b/po/is.po
@@ -0,0 +1,282 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Git\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-05-30 19:24+0000\n"
+"PO-Revision-Date: 2010-06-01 22:15+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"
+
+#: wt-status.c:53
+msgid "# Unmerged paths:"
+msgstr ""
+
+#: wt-status.c:59 wt-status.c:76
+#, c-format
+msgid "#   (use \"git reset %s <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:61 wt-status.c:78
+msgid "#   (use \"git rm --cached <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:62
+msgid "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"
+msgstr ""
+
+#: wt-status.c:63 wt-status.c:79 wt-status.c:98 wt-status.c:110
+#: wt-status.c:115 wt-status.c:638 wt-status.c:640
+msgid "#"
+msgstr ""
+
+#: wt-status.c:70
+msgid "# Changes to be committed:"
+msgstr ""
+
+#: wt-status.c:88
+msgid "# Changed but not updated:"
+msgstr ""
+
+#: wt-status.c:92
+msgid "#   (use \"git add <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:94
+msgid "#   (use \"git add/rm <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:95
+msgid ""
+"#   (use \"git checkout -- <file>...\" to discard changes in working "
+"directory)"
+msgstr ""
+
+#: wt-status.c:97
+msgid "#   (commit or discard the untracked or modified content in submodules)"
+msgstr ""
+
+#: wt-status.c:106
+#, c-format
+msgid "# %s files:"
+msgstr ""
+
+#: wt-status.c:109
+#, c-format
+msgid "#   (use \"git %s <file>...\" to include in what will be committed)"
+msgstr ""
+
+#: wt-status.c:126
+msgid "bug"
+msgstr ""
+
+#: wt-status.c:129 wt-status.c:182
+msgid "#\t"
+msgstr ""
+
+#: wt-status.c:131
+msgid "both deleted:"
+msgstr ""
+
+#: wt-status.c:132
+msgid "added by us:"
+msgstr ""
+
+#: wt-status.c:133
+msgid "deleted by them:"
+msgstr ""
+
+#: wt-status.c:134
+msgid "added by them:"
+msgstr ""
+
+#: wt-status.c:135
+msgid "deleted by us:"
+msgstr ""
+
+#: wt-status.c:136
+msgid "both added:"
+msgstr ""
+
+#: wt-status.c:137
+msgid "both modified:"
+msgstr ""
+
+#: wt-status.c:139
+#, c-format
+msgid "%-20s%s\n"
+msgstr ""
+
+#: wt-status.c:165
+msgid " ("
+msgstr ""
+
+#: wt-status.c:167
+msgid "new commits, "
+msgstr ""
+
+#: wt-status.c:169
+msgid "modified content, "
+msgstr ""
+
+#: wt-status.c:171
+msgid "untracked content, "
+msgstr ""
+
+#: wt-status.c:185
+#, c-format
+msgid "new file:   %s"
+msgstr ""
+
+#: wt-status.c:188
+#, c-format
+msgid "copied:     %s -> %s"
+msgstr ""
+
+#: wt-status.c:191
+#, c-format
+msgid "deleted:    %s"
+msgstr ""
+
+#: wt-status.c:194
+#, c-format
+msgid "modified:   %s"
+msgstr ""
+
+#: wt-status.c:197
+#, c-format
+msgid "renamed:    %s -> %s"
+msgstr ""
+
+#: wt-status.c:200
+#, c-format
+msgid "typechange: %s"
+msgstr ""
+
+#: wt-status.c:203
+#, c-format
+msgid "unknown:    %s"
+msgstr ""
+
+#: wt-status.c:206
+#, c-format
+msgid "unmerged:   %s"
+msgstr ""
+
+#: wt-status.c:209
+#, c-format
+msgid "bug: unhandled diff status %c"
+msgstr ""
+
+#: wt-status.c:212
+#, c-format
+msgid "%s"
+msgstr ""
+
+#: wt-status.c:215
+#, c-format
+msgid "\n"
+msgstr ""
+
+#: wt-status.c:622
+msgid "On branch "
+msgstr "Á greininni "
+
+#: wt-status.c:629
+msgid "Not currently on any branch."
+msgstr "Ekki á neinni grein."
+
+#: wt-status.c:639
+msgid "# Initial commit"
+msgstr ""
+
+#: wt-status.c:651
+msgid "Untracked"
+msgstr ""
+
+#: wt-status.c:651
+msgid "add"
+msgstr ""
+
+#: wt-status.c:653
+msgid "Ignored"
+msgstr ""
+
+#: wt-status.c:653
+msgid "add -f"
+msgstr ""
+
+#: wt-status.c:655
+#, c-format
+msgid "# Untracked files not listed%s\n"
+msgstr ""
+
+#: wt-status.c:657
+msgid " (use -u option to show untracked files)"
+msgstr ""
+
+#: wt-status.c:663
+#, c-format
+msgid "# No changes\n"
+msgstr "# Engar breytingar\n"
+
+#: wt-status.c:667
+#, c-format
+msgid "no changes added to commit%s\n"
+msgstr ""
+
+#: wt-status.c:669
+msgid " (use \"git add\" and/or \"git commit -a\")"
+msgstr ""
+
+#: wt-status.c:671
+#, c-format
+msgid "nothing added to commit but untracked files present%s\n"
+msgstr ""
+
+#: wt-status.c:673
+msgid " (use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:676
+msgid " (create/copy files and use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:678 wt-status.c:681
+#, c-format
+msgid "nothing to commit%s\n"
+msgstr ""
+
+#: wt-status.c:679
+msgid " (use -u to show untracked files)"
+msgstr ""
+
+#: wt-status.c:682
+msgid " (working directory clean)"
+msgstr ""
+
+#: git-pull.sh:124
+msgid "Fetching tags only, you probably meant:"
+msgstr "Næ aðeins í tögg, þú áttir líkast til við:"
+
+#: git-pull.sh:125
+msgid "  git fetch --tags"
+msgstr ""
+
+#: git-pull.sh:157
+msgid ""
+"You are not currently on a branch, so I cannot use any\n"
+"'branch.<branchname>.merge' in your configuration file.\n"
+"Please specify which remote branch you want to use on the command\n"
+"line and try again (e.g. 'git pull <repository> <refspec>').\n"
+"See git-pull(1) for details."
+msgstr ""
+
+#: git-send-email.perl:678
+#, perl-format
+msgid "Emails will be sent from: %s\n"
+msgstr "Póstarnir verða sendir frá: %s\n"
-- 
1.7.1.251.gec7f5.dirty
Johannes Sixt· Jun 2, 2010, 06:32 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v3 0/7] Add internationalization support to Git

Am 6/2/2010 2:11, schrieb Ævar Arnfjörð Bjarmason:
>     +export GIT_TEXTDOMAINDIR=`pwd`/../share/locale
This works only in some shells for two reasons:
- 'export foo=bar' syntax is not understood everywhere;
- if it is understood, then some shells still split the expansion in
'export foo=`pwd`' at white-space.
Use 'foo=bar; export foo', like you did elsewhere.
>      test_expect_success 'sanity: $TEXTDOMAINDIR exists' '
>     -	test -d $TEXTDOMAINDIR
>     +	test -d $TEXTDOMAINDIR &&
>     +	test $TEXTDOMAINDIR = $GIT_TEXTDOMAINDIR
Double-quotes, double-quotes!
-- Hannes
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 22:33 UTC · re: Ævar Arnfjörð Bjarmason · lore

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

This is version 5 of the patch series to make Git optionally translatable with GNU gettext.

I split and squashed the whole thing down into just two patches. The first commit adds all the needed core infrastructure, and the second commit contains an initial stab at starting translation for the core tools, along with some tests that could be added as a result.

The changes this time around are:
  * Testing testing & more testing. Including testing for xgettext
    message extraction, that .po files are generated correctly etc.
  * There's now a git-sh-i18n library for the shell, so gettext.sh
    isn't loaded as part of git-sh-setup.
  * Shell portability fixes suggested by Johannes Sixt
  * Other misc suggestions and fixes Jakub Narebski, Peter Krefting,
    and Jonathan Nieder pointed out were incorporated.
  * I did not change over to Locale::TextDomain for Perl. I wrote a
    version of Git::I18N in it and found that it was more trouble than
    it was worth

I think (this time around, really) that this series is ready for inclusion unless some issue is raised that I've missed. But to play devil's advocate this is what might prevent it from being included.

  * gettext is on by default. I think that's a feature, but Git won't
    build by default on e.g. Solaris unless the packager adds
    NO_GETTEXT=YesPlease to the make arguments. Of course if they use
    ./configure it'll detect if libintl.h exists and act
    appropriately.
    I think gettext on by default is a feature, it's equivalent to
    what we do for our other /*/po/ stuff. But pending discussion of
    this I haven't updated e.g. INSTALL. Comments welcome.
  * There isn't documentation on how to use this from a developer and
    maintainer standpoint. And there's no docs for translators.
    Where would the developer/maintainer docs belong? But for most of
    the things such a documentation would include I think other
    existing resources would do fine, e.g. the GNU gettext manual.
Ævar Arnfjörð Bjarmason (2):
  Add infrastructure for translating Git with gettext
  Add initial C, Shell and Perl gettext translations
 .gitignore                         |    2 +
 Makefile                           |   44 ++++++++++-
 config.mak.in                      |    1 +
 configure.ac                       |    6 ++
 gettext.c                          |   25 ++++++
 gettext.h                          |   13 +++
 git-pull.sh                        |   16 ++--
 git-send-email.perl                |    3 +-
 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                           |   71 +++++++++++++++++
 t/t0200-gettext.sh                 |  146 ++++++++++++++++++++++++++++++++++++
 t/t0200-gettext/test.c             |   10 +++
 t/t0200-gettext/test.perl          |   11 +++
 t/t0200-gettext/test.sh            |   14 ++++
 t/t0201-gettext-shell-fallbacks.sh |   40 ++++++++++
 t/test-lib.sh                      |    1 +
 wt-status.c                        |  107 +++++++++++++-------------
 22 files changed, 576 insertions(+), 65 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
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 22:33 UTC · re: Ævar Arnfjörð Bjarmason · lore

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

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
Show changes to 19 files +447 −4

.gitignore, Makefile, config.mak.in, configure.ac, gettext.c, gettext.h, git-sh-i18n.sh, git.c, perl/Git/I18N.pm, perl/Makefile, perl/Makefile.PL, po/.gitignore, po/is.po, t/t0200-gettext.sh, t/t0200-gettext/test.c, t/t0200-gettext/test.perl, t/t0200-gettext/test.sh, t/t0201-gettext-shell-fallbacks.sh, t/test-lib.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
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 22:33 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v5 2/2] Add initial C, Shell and Perl gettext translations

Change the git status, git pull, and git send-email commands to have at least one translatable string. Each command uses a different core language, so this makes a good example of how C, Shell and Perl programs can be translated using gettext.

Since this introduces translation into the real Git tools more tests can be added to check if they translations actually work for real core tools.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-pull.sh            |   16 ++++---
 git-send-email.perl    |    3 +-
 po/is.po               |   27 +++++++++++-
 t/t0200-gettext.sh     |   42 +++++++++++++++++++
 t/t0200-gettext/test.c |    1 -
 wt-status.c            |  107 ++++++++++++++++++++++++------------------------
 6 files changed, 132 insertions(+), 64 deletions(-)
Show changes to 6 files +132 −64

git-pull.sh, git-send-email.perl, po/is.po, t/t0200-gettext.sh, t/t0200-gettext/test.c, wt-status.c

diff --git a/git-pull.sh b/git-pull.sh
index 1a4729f..0d95722 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -9,6 +9,7 @@ LONG_USAGE='Fetch one or more remote refs and merge it/them into the current HEA
 SUBDIRECTORY_OK=Yes
 OPTIONS_SPEC=
 . git-sh-setup
+. git-sh-i18n
 set_reflog_action "pull $*"
 require_work_tree
 cd_to_toplevel
@@ -121,8 +122,8 @@ error_on_no_merge_candidates () {
 	do
 		case "$opt" in
 		-t|--t|--ta|--tag|--tags)
-			echo "Fetching tags only, you probably meant:"
-			echo "  git fetch --tags"
+			gettext "Fetching tags only, you probably meant:"; echo
+			gettext "  git fetch --tags"; echo
 			exit 1
 		esac
 	done
@@ -154,11 +155,12 @@ error_on_no_merge_candidates () {
 		echo "a branch. Because this is not the default configured remote"
 		echo "for your current branch, you must specify a branch on the command line."
 	elif [ -z "$curr_branch" ]; then
-		echo "You are not currently on a branch, so I cannot use any"
-		echo "'branch.<branchname>.merge' in your configuration file."
-		echo "Please specify which remote branch you want to use on the command"
-		echo "line and try again (e.g. 'git pull <repository> <refspec>')."
-		echo "See git-pull(1) for details."
+		gettext "You are not currently on a branch, so I cannot use any
+'branch.<branchname>.merge' in your configuration file.
+Please specify which remote branch you want to use on the command
+line and try again (e.g. 'git pull <repository> <refspec>').
+See git-pull(1) for details.";
+		echo
 	elif [ -z "$upstream" ]; then
 		echo "You asked me to pull without telling me which branch you"
 		echo "want to $op_type $op_prep, and 'branch.${curr_branch}.merge' in"
diff --git a/git-send-email.perl b/git-send-email.perl
index 111c981..4977fdf 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -26,6 +26,7 @@ use Term::ANSIColor;
 use File::Temp qw/ tempdir tempfile /;
 use Error qw(:try);
 use Git;
+use Git::I18N;
 
 Getopt::Long::Configure qw/ pass_through /;
 
@@ -674,7 +675,7 @@ if (!defined $sender) {
 	$sender = $repoauthor || $repocommitter || '';
 	$sender = ask("Who should the emails appear to be from? [$sender] ",
 	              default => $sender);
-	print "Emails will be sent from: ", $sender, "\n";
+	printf __("Emails will be sent from: %s\n"), $sender;
 	$prompting++;
 }
 
diff --git a/po/is.po b/po/is.po
index ac4317b..72437c3 100644
--- a/po/is.po
+++ b/po/is.po
@@ -2,8 +2,8 @@ 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"
+"POT-Creation-Date: 2010-06-02 19:43+0000\n"
+"PO-Revision-Date: 2010-06-02 19:35+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"
@@ -12,6 +12,20 @@ msgstr ""
 "X-Poedit-Language: English\n"
 "X-Poedit-SourceCharset: utf-8\n"
 
+#: wt-status.c:63 wt-status.c:79 wt-status.c:98 wt-status.c:110
+#: wt-status.c:622
+msgid "On branch "
+msgstr "Á greininni "
+
+#: wt-status.c:629
+msgid "Not currently on any branch."
+msgstr "Ekki á neinni grein."
+
+#: wt-status.c:663
+#, c-format
+msgid "# No changes\n"
+msgstr "# Engar breytingar\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"
@@ -23,6 +37,10 @@ msgstr "TILRAUN: C tilraunastrengur"
 msgid "TEST: A C test string %s"
 msgstr "TILRAUN: C tilraunastrengur %s"
 
+#: git-pull.sh:124
+msgid "Fetching tags only, you probably meant:"
+msgstr "Næ aðeins í tögg, þú áttir líkast til við:"
+
 #. TRANSLATORS: This is a test. You don't need to translate it.
 #: t/t0200-gettext/test.sh:8
 msgid "TEST: A Shell test string"
@@ -34,6 +52,11 @@ msgstr "TILRAUN: Skeljartilraunastrengur"
 msgid "TEST: A Shell test $variable"
 msgstr "TILRAUN: Skeljartilraunastrengur með breytunni $variable"
 
+#: git-send-email.perl:678
+#, perl-format
+msgid "Emails will be sent from: %s\n"
+msgstr "Póstarnir verða sendir frá: %s\n"
+
 #. 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.
diff --git a/t/t0200-gettext.sh b/t/t0200-gettext.sh
index 7b01ce4..95f05ec 100755
--- a/t/t0200-gettext.sh
+++ b/t/t0200-gettext.sh
@@ -101,4 +101,46 @@ test_expect_success 'xgettext: Comment extraction with --add-comments' '
     test_cmp expect actual
 '
 
+# Actually execute some C, Perl and Shell code that uses Gettext
+test_expect_success 'C: git-status reads our message catalog ' '
+    > foo &&
+    test_commit "some-file" &&
+    git checkout -b topic/gettext-testing &&
+    LANGUAGE=C LC_ALL=C git status | grep topic/gettext-testing > expect &&
+    echo "# On branch topic/gettext-testing" > actual &&
+    test_cmp expect actual &&
+
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 git status | grep topic/gettext-testing > expect &&
+    echo "# Á greininni topic/gettext-testing" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'Perl: Git::I18N reads our message catalog ' '
+    # Bail out if the system doesn not have Locale::Messages
+    perl -MLocale::Messages -e1 2>/dev/null || return 0
+
+    echo "On branch " > expect &&
+    LANGUAGE=C LC_ALL=C perl -I"$D/../perl" -MGit::I18N -le "print __(q[On branch ])" > actual &&
+    test_cmp expect actual &&
+
+    echo "Á greininni " > expect &&
+    LANGUAGE=is LC_ALL=is_IS.UTF-8 perl -I"$D/../perl" -MGit::I18N -le "print __(q[On branch ])" > actual &&
+    test_cmp expect actual
+'
+
+test_expect_success 'Shell: git-pull reads our message catalog' '
+    # Repository for testing
+    mkdir parent &&
+    (cd parent && git init &&
+     echo one >file && git add file &&
+     git commit -m one) &&
+
+    # Actual test
+    (cd parent &&
+    (LANGUAGE=C LC_ALL=C git pull --tags "../" >out 2>err);
+    grep "Fetching tags only" err &&
+    (LANGUAGE=is LC_ALL=is_IS.UTF-8 git pull --tags ../ >out 2>err || :) &&
+    grep "Næ aðeins í" err)
+'
+
 test_done
diff --git a/t/t0200-gettext/test.c b/t/t0200-gettext/test.c
index 9fa4c23..171bcc6 100644
--- a/t/t0200-gettext/test.c
+++ b/t/t0200-gettext/test.c
@@ -8,4 +8,3 @@ int main(void)
 	/* TRANSLATORS: This is a test. You don't need to translate it. */
 	printf(_("TEST: A C test string %s"), "variable");
 }
-	
diff --git a/wt-status.c b/wt-status.c
index 14e0acc..484a866 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -9,6 +9,7 @@
 #include "quote.h"
 #include "run-command.h"
 #include "remote.h"
+#include "gettext.h"
 
 static char default_wt_status_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_NORMAL, /* WT_STATUS_HEADER */
@@ -49,16 +50,16 @@ static void wt_status_print_unmerged_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Unmerged paths:");
+	color_fprintf_ln(s->fp, c, _("# Unmerged paths:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		;
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
 	color_fprintf_ln(s->fp, c, "#");
 }
 
@@ -66,15 +67,15 @@ static void wt_status_print_cached_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changes to be committed:");
+	color_fprintf_ln(s->fp, c, _("# Changes to be committed:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		; /* NEEDSWORK: use "git reset --unresolve"??? */
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
 	color_fprintf_ln(s->fp, c, "#");
 }
 
@@ -84,16 +85,16 @@ static void wt_status_print_dirty_header(struct wt_status *s,
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changed but not updated:");
+	color_fprintf_ln(s->fp, c, _("# Changed but not updated:"));
 	if (!advice_status_hints)
 		return;
 	if (!has_deleted)
-		color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to update what will be committed)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add <file>...\" to update what will be committed)"));
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" to update what will be committed)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git checkout -- <file>...\" to discard changes in working directory)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" to update what will be committed)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git checkout -- <file>...\" to discard changes in working directory)"));
 	if (has_dirty_submodules)
-		color_fprintf_ln(s->fp, c, "#   (commit or discard the untracked or modified content in submodules)");
+		color_fprintf_ln(s->fp, c, _("#   (commit or discard the untracked or modified content in submodules)"));
 	color_fprintf_ln(s->fp, c, "#");
 }
 
@@ -102,10 +103,10 @@ static void wt_status_print_other_header(struct wt_status *s,
 					 const char *how)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
-	color_fprintf_ln(s->fp, c, "# %s files:", what);
+	color_fprintf_ln(s->fp, c, _("# %s files:"), what);
 	if (!advice_status_hints)
 		return;
-	color_fprintf_ln(s->fp, c, "#   (use \"git %s <file>...\" to include in what will be committed)", how);
+	color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to include in what will be committed)"), how);
 	color_fprintf_ln(s->fp, c, "#");
 }
 
@@ -122,20 +123,20 @@ static void wt_status_print_unmerged_data(struct wt_status *s,
 	const char *c = color(WT_STATUS_UNMERGED, s);
 	struct wt_status_change_data *d = it->util;
 	struct strbuf onebuf = STRBUF_INIT;
-	const char *one, *how = "bug";
+	const char *one, *how = _("bug");
 
 	one = quote_path(it->string, -1, &onebuf, s->prefix);
 	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
 	switch (d->stagemask) {
-	case 1: how = "both deleted:"; break;
-	case 2: how = "added by us:"; break;
-	case 3: how = "deleted by them:"; break;
-	case 4: how = "added by them:"; break;
-	case 5: how = "deleted by us:"; break;
-	case 6: how = "both added:"; break;
-	case 7: how = "both modified:"; break;
+	case 1: how = _("both deleted:"); break;
+	case 2: how = _("added by us:"); break;
+	case 3: how = _("deleted by them:"); break;
+	case 4: how = _("added by them:"); break;
+	case 5: how = _("deleted by us:"); break;
+	case 6: how = _("both added:"); break;
+	case 7: how = _("both modified:"); break;
 	}
-	color_fprintf(s->fp, c, "%-20s%s\n", how, one);
+	color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);
 	strbuf_release(&onebuf);
 }
 
@@ -163,11 +164,11 @@ static void wt_status_print_change_data(struct wt_status *s,
 		if (d->new_submodule_commits || d->dirty_submodule) {
 			strbuf_addstr(&extra, " (");
 			if (d->new_submodule_commits)
-				strbuf_addf(&extra, "new commits, ");
+				strbuf_addf(&extra, _("new commits, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
-				strbuf_addf(&extra, "modified content, ");
+				strbuf_addf(&extra, _("modified content, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_UNTRACKED)
-				strbuf_addf(&extra, "untracked content, ");
+				strbuf_addf(&extra, _("untracked content, "));
 			strbuf_setlen(&extra, extra.len - 2);
 			strbuf_addch(&extra, ')');
 		}
@@ -178,34 +179,34 @@ static void wt_status_print_change_data(struct wt_status *s,
 	one = quote_path(one_name, -1, &onebuf, s->prefix);
 	two = quote_path(two_name, -1, &twobuf, s->prefix);
 
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (status) {
 	case DIFF_STATUS_ADDED:
-		color_fprintf(s->fp, c, "new file:   %s", one);
+		color_fprintf(s->fp, c, _("new file:   %s"), one);
 		break;
 	case DIFF_STATUS_COPIED:
-		color_fprintf(s->fp, c, "copied:     %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("copied:     %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_DELETED:
-		color_fprintf(s->fp, c, "deleted:    %s", one);
+		color_fprintf(s->fp, c, _("deleted:    %s"), one);
 		break;
 	case DIFF_STATUS_MODIFIED:
-		color_fprintf(s->fp, c, "modified:   %s", one);
+		color_fprintf(s->fp, c, _("modified:   %s"), one);
 		break;
 	case DIFF_STATUS_RENAMED:
-		color_fprintf(s->fp, c, "renamed:    %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("renamed:    %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_TYPE_CHANGED:
-		color_fprintf(s->fp, c, "typechange: %s", one);
+		color_fprintf(s->fp, c, _("typechange: %s"), one);
 		break;
 	case DIFF_STATUS_UNKNOWN:
-		color_fprintf(s->fp, c, "unknown:    %s", one);
+		color_fprintf(s->fp, c, _("unknown:    %s"), one);
 		break;
 	case DIFF_STATUS_UNMERGED:
-		color_fprintf(s->fp, c, "unmerged:   %s", one);
+		color_fprintf(s->fp, c, _("unmerged:   %s"), one);
 		break;
 	default:
-		die("bug: unhandled diff status %c", status);
+		die(_("bug: unhandled diff status %c"), status);
 	}
 	if (extra.len) {
 		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s", extra.buf);
@@ -618,14 +619,14 @@ void wt_status_print(struct wt_status *s)
 	const char *branch_color = color(WT_STATUS_HEADER, s);
 
 	if (s->branch) {
-		const char *on_what = "On branch ";
+		const char *on_what = _("On branch ");
 		const char *branch_name = s->branch;
 		if (!prefixcmp(branch_name, "refs/heads/"))
 			branch_name += 11;
 		else if (!strcmp(branch_name, "HEAD")) {
 			branch_name = "";
 			branch_color = color(WT_STATUS_NOBRANCH, s);
-			on_what = "Not currently on any branch.";
+			on_what = _("Not currently on any branch.");
 		}
 		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "# ");
 		color_fprintf_ln(s->fp, branch_color, "%s%s", on_what, branch_name);
@@ -635,7 +636,7 @@ void wt_status_print(struct wt_status *s)
 
 	if (s->is_initial) {
 		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "# Initial commit");
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("# Initial commit"));
 		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
 	}
 
@@ -647,38 +648,38 @@ void wt_status_print(struct wt_status *s)
 		wt_status_print_submodule_summary(s, 1);  /* unstaged */
 	}
 	if (s->show_untracked_files) {
-		wt_status_print_other(s, &s->untracked, "Untracked", "add");
+		wt_status_print_other(s, &s->untracked, _("Untracked"), _("add"));
 		if (s->show_ignored_files)
-			wt_status_print_other(s, &s->ignored, "Ignored", "add -f");
+			wt_status_print_other(s, &s->ignored, _("Ignored"), _("add -f"));
 	} else if (s->commitable)
-		fprintf(s->fp, "# Untracked files not listed%s\n",
+		fprintf(s->fp, _("# Untracked files not listed%s\n"),
 			advice_status_hints
-			? " (use -u option to show untracked files)" : "");
+			? _(" (use -u option to show untracked files)") : "");
 
 	if (s->verbose)
 		wt_status_print_verbose(s);
 	if (!s->commitable) {
 		if (s->amend)
-			fprintf(s->fp, "# No changes\n");
+			fprintf(s->fp, _("# No changes\n"));
 		else if (s->nowarn)
 			; /* nothing */
 		else if (s->workdir_dirty)
-			printf("no changes added to commit%s\n",
+			printf(_("no changes added to commit%s\n"),
 				advice_status_hints
-				? " (use \"git add\" and/or \"git commit -a\")" : "");
+				? _(" (use \"git add\" and/or \"git commit -a\")") : "");
 		else if (s->untracked.nr)
-			printf("nothing added to commit but untracked files present%s\n",
+			printf(_("nothing added to commit but untracked files present%s\n"),
 				advice_status_hints
-				? " (use \"git add\" to track)" : "");
+				? _(" (use \"git add\" to track)") : "");
 		else if (s->is_initial)
 			printf("nothing to commit%s\n", advice_status_hints
-				? " (create/copy files and use \"git add\" to track)" : "");
+				? _(" (create/copy files and use \"git add\" to track)") : "");
 		else if (!s->show_untracked_files)
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (use -u to show untracked files)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (use -u to show untracked files)") : "");
 		else
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (working directory clean)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (working directory clean)") : "");
 	}
 }
 
-- 
1.7.1.242.g290e3.dirty
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 1/7] Add infrastructure for translating Git with gettext

From: Jeff Epler <jepler@unpythonic.net>

Change the build process to use GNU's libintl if it's available. If not we define our own skeleton replacement functions which degrade gracefully to English.

Gettext .mo files will be installed and looked for in $(prefix)/share/locale. GIT_LOCALEDIR can also be set to override that, this variable is only intended for testing Git itself.

Signed-off-by: Jeff Epler <jepler@unpythonic.net>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore    |    1 +
 Makefile      |   30 +++++++++++++
 config.mak.in |    1 +
 configure.ac  |    6 +++
 gettext.c     |   25 +++++++++++
 gettext.h     |   13 ++++++
 git.c         |    3 +
 po/.gitignore |    1 +
 wt-status.c   |  129 +++++++++++++++++++++++++++++----------------------------
 9 files changed, 145 insertions(+), 64 deletions(-)
 create mode 100644 gettext.c
 create mode 100644 gettext.h
 create mode 100644 po/.gitignore
Show changes to 9 files +145 −64

.gitignore, Makefile, config.mak.in, configure.ac, gettext.c, gettext.h, git.c, po/.gitignore, wt-status.c

diff --git a/.gitignore b/.gitignore
index 14e2b6b..d22c0e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -204,3 +204,4 @@
 *.pdb
 /Debug/
 /Release/
+/share/
diff --git a/Makefile b/Makefile
index d5d6565..4de0627 100644
--- a/Makefile
+++ b/Makefile
@@ -297,6 +297,8 @@ RPMBUILD = rpmbuild
 TCL_PATH = tclsh
 TCLTK_PATH = wish
 PTHREAD_LIBS = -lpthread
+XGETTEXT = xgettext
+MSGFMT = msgfmt
 
 export TCL_PATH TCLTK_PATH
 
@@ -523,6 +525,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 +567,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 +1390,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 +1425,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,6 +1453,7 @@ gitexecdir_SQ = $(subst ','\'',$(gitexecdir))
 template_dir_SQ = $(subst ','\'',$(template_dir))
 htmldir_SQ = $(subst ','\'',$(htmldir))
 prefix_SQ = $(subst ','\'',$(prefix))
+sharedir_SQ = $(subst ','\'',$(sharedir))
 
 SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))
 PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))
@@ -1868,6 +1880,17 @@ cscope:
 	$(RM) cscope*
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
+pot:
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+
+POFILES := $(wildcard po/*.po)
+MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
+MODIRS := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/,$(POFILES))
+all:: $(MOFILES)
+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 +2003,9 @@ 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)'
+	$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(sharedir_SQ)/locale'
+	(cd share && tar cf - locale) | \
+		(cd '$(DESTDIR_SQ)$(sharedir_SQ)' && umask 022 && tar xof -)
 	$(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install
 ifndef NO_PERL
 	$(MAKE) -C perl prefix='$(prefix_SQ)' DESTDIR='$(DESTDIR_SQ)' install
@@ -2127,6 +2153,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..c67e5ca
--- /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.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/po/.gitignore b/po/.gitignore
new file mode 100644
index 0000000..221000e
--- /dev/null
+++ b/po/.gitignore
@@ -0,0 +1 @@
+/*.pot
diff --git a/wt-status.c b/wt-status.c
index 14e0acc..70b4293 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -9,6 +9,7 @@
 #include "quote.h"
 #include "run-command.h"
 #include "remote.h"
+#include "gettext.h"
 
 static char default_wt_status_colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_NORMAL, /* WT_STATUS_HEADER */
@@ -49,33 +50,33 @@ static void wt_status_print_unmerged_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Unmerged paths:");
+	color_fprintf_ln(s->fp, c, _("# Unmerged paths:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		;
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_cached_header(struct wt_status *s)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changes to be committed:");
+	color_fprintf_ln(s->fp, c, _("# Changes to be committed:"));
 	if (!advice_status_hints)
 		return;
 	if (s->in_merge)
 		; /* NEEDSWORK: use "git reset --unresolve"??? */
 	else if (!s->is_initial)
-		color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
+		color_fprintf_ln(s->fp, c, _("#   (use \"git reset %s <file>...\" to unstage)"), s->reference);
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git rm --cached <file>...\" to unstage)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_dirty_header(struct wt_status *s,
@@ -84,17 +85,17 @@ static void wt_status_print_dirty_header(struct wt_status *s,
 {
 	const char *c = color(WT_STATUS_HEADER, s);
 
-	color_fprintf_ln(s->fp, c, "# Changed but not updated:");
+	color_fprintf_ln(s->fp, c, _("# Changed but not updated:"));
 	if (!advice_status_hints)
 		return;
 	if (!has_deleted)
-		color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to update what will be committed)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add <file>...\" to update what will be committed)"));
 	else
-		color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" to update what will be committed)");
-	color_fprintf_ln(s->fp, c, "#   (use \"git checkout -- <file>...\" to discard changes in working directory)");
+		color_fprintf_ln(s->fp, c, _("#   (use \"git add/rm <file>...\" to update what will be committed)"));
+	color_fprintf_ln(s->fp, c, _("#   (use \"git checkout -- <file>...\" to discard changes in working directory)"));
 	if (has_dirty_submodules)
-		color_fprintf_ln(s->fp, c, "#   (commit or discard the untracked or modified content in submodules)");
-	color_fprintf_ln(s->fp, c, "#");
+		color_fprintf_ln(s->fp, c, _("#   (commit or discard the untracked or modified content in submodules)"));
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_other_header(struct wt_status *s,
@@ -102,16 +103,16 @@ static void wt_status_print_other_header(struct wt_status *s,
 					 const char *how)
 {
 	const char *c = color(WT_STATUS_HEADER, s);
-	color_fprintf_ln(s->fp, c, "# %s files:", what);
+	color_fprintf_ln(s->fp, c, _("# %s files:"), what);
 	if (!advice_status_hints)
 		return;
-	color_fprintf_ln(s->fp, c, "#   (use \"git %s <file>...\" to include in what will be committed)", how);
-	color_fprintf_ln(s->fp, c, "#");
+	color_fprintf_ln(s->fp, c, _("#   (use \"git %s <file>...\" to include in what will be committed)"), how);
+	color_fprintf_ln(s->fp, c, _("#"));
 }
 
 static void wt_status_print_trailer(struct wt_status *s)
 {
-	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+	color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 }
 
 #define quote_path quote_path_relative
@@ -122,20 +123,20 @@ static void wt_status_print_unmerged_data(struct wt_status *s,
 	const char *c = color(WT_STATUS_UNMERGED, s);
 	struct wt_status_change_data *d = it->util;
 	struct strbuf onebuf = STRBUF_INIT;
-	const char *one, *how = "bug";
+	const char *one, *how = _("bug");
 
 	one = quote_path(it->string, -1, &onebuf, s->prefix);
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (d->stagemask) {
-	case 1: how = "both deleted:"; break;
-	case 2: how = "added by us:"; break;
-	case 3: how = "deleted by them:"; break;
-	case 4: how = "added by them:"; break;
-	case 5: how = "deleted by us:"; break;
-	case 6: how = "both added:"; break;
-	case 7: how = "both modified:"; break;
+	case 1: how = _("both deleted:"); break;
+	case 2: how = _("added by us:"); break;
+	case 3: how = _("deleted by them:"); break;
+	case 4: how = _("added by them:"); break;
+	case 5: how = _("deleted by us:"); break;
+	case 6: how = _("both added:"); break;
+	case 7: how = _("both modified:"); break;
 	}
-	color_fprintf(s->fp, c, "%-20s%s\n", how, one);
+	color_fprintf(s->fp, c, _("%-20s%s\n"), how, one);
 	strbuf_release(&onebuf);
 }
 
@@ -161,13 +162,13 @@ static void wt_status_print_change_data(struct wt_status *s,
 		break;
 	case WT_STATUS_CHANGED:
 		if (d->new_submodule_commits || d->dirty_submodule) {
-			strbuf_addstr(&extra, " (");
+			strbuf_addstr(&extra, _(" ("));
 			if (d->new_submodule_commits)
-				strbuf_addf(&extra, "new commits, ");
+				strbuf_addf(&extra, _("new commits, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
-				strbuf_addf(&extra, "modified content, ");
+				strbuf_addf(&extra, _("modified content, "));
 			if (d->dirty_submodule & DIRTY_SUBMODULE_UNTRACKED)
-				strbuf_addf(&extra, "untracked content, ");
+				strbuf_addf(&extra, _("untracked content, "));
 			strbuf_setlen(&extra, extra.len - 2);
 			strbuf_addch(&extra, ')');
 		}
@@ -178,40 +179,40 @@ static void wt_status_print_change_data(struct wt_status *s,
 	one = quote_path(one_name, -1, &onebuf, s->prefix);
 	two = quote_path(two_name, -1, &twobuf, s->prefix);
 
-	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "#\t");
+	color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("#\t"));
 	switch (status) {
 	case DIFF_STATUS_ADDED:
-		color_fprintf(s->fp, c, "new file:   %s", one);
+		color_fprintf(s->fp, c, _("new file:   %s"), one);
 		break;
 	case DIFF_STATUS_COPIED:
-		color_fprintf(s->fp, c, "copied:     %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("copied:     %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_DELETED:
-		color_fprintf(s->fp, c, "deleted:    %s", one);
+		color_fprintf(s->fp, c, _("deleted:    %s"), one);
 		break;
 	case DIFF_STATUS_MODIFIED:
-		color_fprintf(s->fp, c, "modified:   %s", one);
+		color_fprintf(s->fp, c, _("modified:   %s"), one);
 		break;
 	case DIFF_STATUS_RENAMED:
-		color_fprintf(s->fp, c, "renamed:    %s -> %s", one, two);
+		color_fprintf(s->fp, c, _("renamed:    %s -> %s"), one, two);
 		break;
 	case DIFF_STATUS_TYPE_CHANGED:
-		color_fprintf(s->fp, c, "typechange: %s", one);
+		color_fprintf(s->fp, c, _("typechange: %s"), one);
 		break;
 	case DIFF_STATUS_UNKNOWN:
-		color_fprintf(s->fp, c, "unknown:    %s", one);
+		color_fprintf(s->fp, c, _("unknown:    %s"), one);
 		break;
 	case DIFF_STATUS_UNMERGED:
-		color_fprintf(s->fp, c, "unmerged:   %s", one);
+		color_fprintf(s->fp, c, _("unmerged:   %s"), one);
 		break;
 	default:
-		die("bug: unhandled diff status %c", status);
+		die(_("bug: unhandled diff status %c"), status);
 	}
 	if (extra.len) {
-		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "%s", extra.buf);
+		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), _("%s"), extra.buf);
 		strbuf_release(&extra);
 	}
-	fprintf(s->fp, "\n");
+	fprintf(s->fp, _("\n"));
 	strbuf_release(&onebuf);
 	strbuf_release(&twobuf);
 }
@@ -618,14 +619,14 @@ void wt_status_print(struct wt_status *s)
 	const char *branch_color = color(WT_STATUS_HEADER, s);
 
 	if (s->branch) {
-		const char *on_what = "On branch ";
+		const char *on_what = _("On branch ");
 		const char *branch_name = s->branch;
 		if (!prefixcmp(branch_name, "refs/heads/"))
 			branch_name += 11;
 		else if (!strcmp(branch_name, "HEAD")) {
 			branch_name = "";
 			branch_color = color(WT_STATUS_NOBRANCH, s);
-			on_what = "Not currently on any branch.";
+			on_what = _("Not currently on any branch.");
 		}
 		color_fprintf(s->fp, color(WT_STATUS_HEADER, s), "# ");
 		color_fprintf_ln(s->fp, branch_color, "%s%s", on_what, branch_name);
@@ -634,9 +635,9 @@ void wt_status_print(struct wt_status *s)
 	}
 
 	if (s->is_initial) {
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "# Initial commit");
-		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("# Initial commit"));
+		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), _("#"));
 	}
 
 	wt_status_print_updated(s);
@@ -647,38 +648,38 @@ void wt_status_print(struct wt_status *s)
 		wt_status_print_submodule_summary(s, 1);  /* unstaged */
 	}
 	if (s->show_untracked_files) {
-		wt_status_print_other(s, &s->untracked, "Untracked", "add");
+		wt_status_print_other(s, &s->untracked, _("Untracked"), _("add"));
 		if (s->show_ignored_files)
-			wt_status_print_other(s, &s->ignored, "Ignored", "add -f");
+			wt_status_print_other(s, &s->ignored, _("Ignored"), _("add -f"));
 	} else if (s->commitable)
-		fprintf(s->fp, "# Untracked files not listed%s\n",
+		fprintf(s->fp, _("# Untracked files not listed%s\n"),
 			advice_status_hints
-			? " (use -u option to show untracked files)" : "");
+			? _(" (use -u option to show untracked files)") : "");
 
 	if (s->verbose)
 		wt_status_print_verbose(s);
 	if (!s->commitable) {
 		if (s->amend)
-			fprintf(s->fp, "# No changes\n");
+			fprintf(s->fp, _("# No changes\n"));
 		else if (s->nowarn)
 			; /* nothing */
 		else if (s->workdir_dirty)
-			printf("no changes added to commit%s\n",
+			printf(_("no changes added to commit%s\n"),
 				advice_status_hints
-				? " (use \"git add\" and/or \"git commit -a\")" : "");
+				? _(" (use \"git add\" and/or \"git commit -a\")") : "");
 		else if (s->untracked.nr)
-			printf("nothing added to commit but untracked files present%s\n",
+			printf(_("nothing added to commit but untracked files present%s\n"),
 				advice_status_hints
-				? " (use \"git add\" to track)" : "");
+				? _(" (use \"git add\" to track)") : "");
 		else if (s->is_initial)
 			printf("nothing to commit%s\n", advice_status_hints
-				? " (create/copy files and use \"git add\" to track)" : "");
+				? _(" (create/copy files and use \"git add\" to track)") : "");
 		else if (!s->show_untracked_files)
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (use -u to show untracked files)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (use -u to show untracked files)") : "");
 		else
-			printf("nothing to commit%s\n", advice_status_hints
-				? " (working directory clean)" : "");
+			printf(_("nothing to commit%s\n"), advice_status_hints
+				? _(" (working directory clean)") : "");
 	}
 }
 
-- 
1.7.1.248.gb7713.dirty
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 2/7] gettext: Add a Gettext interface for shell scripts

Use GNU's gettext.sh in git-sh-setup if it's available, otherwise fallback on our own custom functions.

A couple of strings in git-pull.sh are now translatable as a proof of concept, including a multiline string.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile        |    7 +++++--
 git-pull.sh     |   15 ++++++++-------
 git-sh-setup.sh |   33 +++++++++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 9 deletions(-)
Show changes to 3 files +46 −9

Makefile, git-pull.sh, git-sh-setup.sh

diff --git a/Makefile b/Makefile
index 4de0627..dce2faa 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 localedir sysconfdir
 
 CC = gcc
 AR = ar
@@ -1455,6 +1456,7 @@ 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))
@@ -1548,6 +1550,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 >$@+
@@ -1881,7 +1884,7 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c)
+	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-pull.sh b/git-pull.sh
index 1a4729f..22a6da2 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -121,8 +121,8 @@ error_on_no_merge_candidates () {
 	do
 		case "$opt" in
 		-t|--t|--ta|--tag|--tags)
-			echo "Fetching tags only, you probably meant:"
-			echo "  git fetch --tags"
+			gettext "Fetching tags only, you probably meant:"; echo
+			gettext "  git fetch --tags"; echo
 			exit 1
 		esac
 	done
@@ -154,11 +154,12 @@ error_on_no_merge_candidates () {
 		echo "a branch. Because this is not the default configured remote"
 		echo "for your current branch, you must specify a branch on the command line."
 	elif [ -z "$curr_branch" ]; then
-		echo "You are not currently on a branch, so I cannot use any"
-		echo "'branch.<branchname>.merge' in your configuration file."
-		echo "Please specify which remote branch you want to use on the command"
-		echo "line and try again (e.g. 'git pull <repository> <refspec>')."
-		echo "See git-pull(1) for details."
+        gettext "You are not currently on a branch, so I cannot use any
+'branch.<branchname>.merge' in your configuration file.
+Please specify which remote branch you want to use on the command
+line and try again (e.g. 'git pull <repository> <refspec>').
+See git-pull(1) for details.";
+        echo
 	elif [ -z "$upstream" ]; then
 		echo "You asked me to pull without telling me which branch you"
 		echo "want to $op_type $op_prep, and 'branch.${curr_branch}.merge' in"
diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 6131670..d67901c 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -211,3 +211,36 @@ case $(uname -s) in
 	}
 	;;
 esac
+
+# Try to use libintl's gettext.sh, or fall back to English if we
+# can't.
+. gettext.sh
+if test $? -eq 0
+then
+    TEXTDOMAIN=git
+    export TEXDTOMAIN
+    TEXTDOMAINDIR="@@LOCALEDIR@@"
+    export TEXTDOMAINDIR
+else
+    # Since GNU gettext.sh isn't available we'll have to define our
+    # own dummy functions.
+
+    # This code adapted from NessusClient-1.0.2's nessusclient-mkcert
+    # by Michel Arboi <arboi@alussinan.org>. The original code is
+    # under the GPLv2.
+
+    # Not everyone has echo -n
+    case $(echo -n) in
+        \-n)    Xn=   ; Xc='\c' ;;
+        *)      Xn=-n ; Xc=
+    esac
+
+    gettext () {
+        echo $Xn "$1" $Xc
+    }
+
+    eval_gettext () {
+        eval_gettext_var="echo $1"
+        echo $Xn `eval $eval_gettext_var` $Xc
+    }
+fi
-- 
1.7.1.248.gb7713.dirty
Johannes Sixt· Jun 2, 2010, 06:32 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v3 2/7] gettext: Add a Gettext interface for shell scripts

[When you resend the series in such a manner that you collect a Cc list manually, please do *not* include me. I'm not interested in this series.]

Am 6/2/2010 1:39, schrieb Ævar Arnfjörð Bjarmason:
Show 5 quoted lines
> +    # Not everyone has echo -n
> +    case $(echo -n) in
> +        \-n)    Xn=   ; Xc='\c' ;;
> +        *)      Xn=-n ; Xc=
> +    esac
Don't use 'echo'; use 'printf %s'.
Show 9 quoted lines
> +
> +    gettext () {
> +        echo $Xn "$1" $Xc
> +    }
> +
> +    eval_gettext () {
> +        eval_gettext_var="echo $1"
> +        echo $Xn `eval $eval_gettext_var` $Xc
> +    }
-- Hannes
Ævar Arnfjörð Bjarmason· Jun 2, 2010, 08:53 UTC · re: Johannes Sixt · lore

Re: [PATCH/RFC v3 2/7] gettext: Add a Gettext interface for shell scripts

On Wed, Jun 2, 2010 at 06:32, Johannes Sixt <j.sixt@viscovery.net> wrote:
Show 11 quoted lines
> [When you resend the series in such a manner that you collect a Cc list
> manually, please do *not* include me. I'm not interested in this series.]
>
> Am 6/2/2010 1:39, schrieb Ævar Arnfjörð Bjarmason:
>> +    # Not everyone has echo -n
>> +    case $(echo -n) in
>> +        \-n)    Xn=   ; Xc='\c' ;;
>> +        *)      Xn=-n ; Xc=
>> +    esac
>
> Don't use 'echo'; use 'printf %s'.

I didn't write this code (as the comment says) but I /thought/ that the whole point of it was to autodetect if echo -n worked, and if so use it because it'd be faster than printf "%s", otherwise use some evil foo\n\c hack.

That hack doesn't work on bash ('\c' produces a space), maybe it doesn't work anywhere. Do you have a shell that doesn't support echo -n? What does it output there?

I wouldn't be surprised if it did need to be changed to printf "%s". I'm just curious where it broke and how.

Johannes Sixt· Jun 2, 2010, 09:38 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v3 2/7] gettext: Add a Gettext interface for shell scripts

Am 6/2/2010 10:53, schrieb Ævar Arnfjörð Bjarmason:
> That hack doesn't work on bash ('\c' produces a space), maybe it
> doesn't work anywhere. Do you have a shell that doesn't support echo
> -n? What does it output there?

It's not just echo -n. You are dealing with text that is outside your control. There are implementations of echo that interpret backslash in the text (like \t, \n etc.). You are risking that they are treated as escape characters rather than printed as-are.

> I wouldn't be surprised if it did need to be changed to printf
> "%s". I'm just curious where it broke and how.
Actually, I don't know where -n does not work.
-- Hannes
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 3/7] gettext: Add a Gettext interface for Perl

Make Git's gettext messages available to Perl programs through Locale::Messages. Gracefully fall back to English on systems that don't contain the module.

This contains some makefile hacks to pass localedir to perl/Makefile. Which in turn passes it to perl.mak, which'll use it to search/replace occurances of ++LOCALEDIR++ in .pm files.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile            |    6 +++-
 git-send-email.perl |    3 +-
 perl/Git/I18N.pm    |   73 +++++++++++++++++++++++++++++++++++++++++++++++++++
 perl/Makefile       |    3 +-
 perl/Makefile.PL    |   14 +++++++++-
 5 files changed, 94 insertions(+), 5 deletions(-)
 create mode 100644 perl/Git/I18N.pm
Show changes to 5 files +94 −5

Makefile, git-send-email.perl, perl/Git/I18N.pm, perl/Makefile, perl/Makefile.PL

diff --git a/Makefile b/Makefile
index dce2faa..3f0e305 100644
--- a/Makefile
+++ b/Makefile
@@ -1505,7 +1505,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
@@ -1884,7 +1884,9 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) -k_ -o po/git.pot $(C_OBJ:o=c) $(SCRIPT_SH)
+	$(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
+	$(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
+	$(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
diff --git a/git-send-email.perl b/git-send-email.perl
index 111c981..b9288af 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -26,6 +26,7 @@ use Term::ANSIColor;
 use File::Temp qw/ tempdir tempfile /;
 use Error qw(:try);
 use Git;
+use Git::I18N;
 
 Getopt::Long::Configure qw/ pass_through /;
 
@@ -674,7 +675,7 @@ if (!defined $sender) {
 	$sender = $repoauthor || $repocommitter || '';
 	$sender = ask("Who should the emails appear to be from? [$sender] ",
 	              default => $sender);
-	print "Emails will be sent from: ", $sender, "\n";
+	printf gettext("Emails will be sent from: %s\n"), $sender;
 	$prompting++;
 }
 
diff --git a/perl/Git/I18N.pm b/perl/Git/I18N.pm
new file mode 100644
index 0000000..568fd3d
--- /dev/null
+++ b/perl/Git/I18N.pm
@@ -0,0 +1,73 @@
+package Git::I18N;
+use strict;
+use warnings;
+use Exporter;
+use base 'Exporter';
+
+our $VERSION = '0.01';
+
+our @EXPORT = qw(gettext);
+our @EXPORT_OK = @EXPORT;
+
+sub __bootstrap_locale_messages {
+	our $TEXTDOMAIN = 'git';
+	our $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
+		*gettext = sub ($) { $_[0] };
+	}
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Git::I18N - Perl interface to Git's Gettext localizations
+
+=head1 SYNOPSIS
+
+	use Git::I18N;
+
+	print gettext("Welcome to Git!\n");
+
+	printf gettext("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 gettext($)
+
+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'
 );
-- 
1.7.1.248.gb7713.dirty
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 4/7] Makefile: Don't install Gettext .mo files if NO_GETTEXT

Change the Makefile not to build/install .mo files as part of the default target if NO_GETTEXT=1 is given.

This was both redundant, and meant that the Perl and Shell programs would use the Gettext strings, since they don't use the git_setup_gettext() function in gettext.c, which is compiled to a stub if NO_GETTEXT=1 is set.

Reported-by: Jakub Narebski <jnareb@gmail.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Makefile |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)
Show changes to Makefile +4 −0
diff --git a/Makefile b/Makefile
index 3f0e305..40eb1a2 100644
--- a/Makefile
+++ b/Makefile
@@ -1891,7 +1891,9 @@ pot:
 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 $@ $<
@@ -2008,9 +2010,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
-- 
1.7.1.248.gb7713.dirty
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 5/7] Makefile: Override --keyword= for all languages

By default xgettext will supply a default list of keywords depending on the --language parameter. Disable this in favor of supplying a list of keywords manually.

This reduced the surface area for potential bugs.
---
 Makefile |    6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)
Show changes to Makefile +3 −3
diff --git a/Makefile b/Makefile
index 40eb1a2..62b8279 100644
--- a/Makefile
+++ b/Makefile
@@ -1884,9 +1884,9 @@ cscope:
 	$(FIND) . -name '*.[hcS]' -print | xargs cscope -b
 
 pot:
-	$(XGETTEXT) --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
-	$(XGETTEXT) --join-existing --output=po/git.pot --language=Shell $(SCRIPT_SH)
-	$(XGETTEXT) --join-existing --output=po/git.pot --language=Perl $(SCRIPT_PERL)
+	$(XGETTEXT) --keyword= --keyword=_ --output=po/git.pot --language=C $(C_OBJ:o=c)
+	$(XGETTEXT) --join-existing --keyword= --keyword=gettext --output=po/git.pot --language=Shell $(SCRIPT_SH)
+	$(XGETTEXT) --join-existing --keyword= --keyword=gettext --output=po/git.pot --language=Perl $(SCRIPT_PERL)
 
 POFILES := $(wildcard po/*.po)
 MOFILES := $(patsubst po/%.po,share/locale/%/LC_MESSAGES/git.mo,$(POFILES))
-- 
1.7.1.248.gb7713.dirty
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 6/7] gettext: Basic sanity tests for Git's Gettext support

---
 t/t0200-gettext.sh |   28 ++++++++++++++++++++++++++++
 1 files changed, 28 insertions(+), 0 deletions(-)
 create mode 100755 t/t0200-gettext.sh
Show changes to t/t0200-gettext.sh +28 −0
diff --git a/t/t0200-gettext.sh b/t/t0200-gettext.sh
new file mode 100755
index 0000000..a8f9edd
--- /dev/null
+++ b/t/t0200-gettext.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+test_description='Gettext support for Git'
+. ./test-lib.sh
+. ../../git-sh-setup
+
+test_expect_success 'sanity: $TEXTDOMAIN is git' '
+	test $TEXTDOMAIN = "git"
+'
+
+test_expect_success 'sanity: $TEXTDOMAINDIR exists' '
+	test -d $TEXTDOMAINDIR 
+'
+
+test_expect_success 'sanity: Icelandic locale was compiled' '
+    test -f $TEXTDOMAINDIR/is/LC_MESSAGES/git.mo
+'
+
+test_expect_success 'sanity: gettext(unknown) is passed through' '
+    gettext "" > expect &&
+    > actual &&
+    test_cmp expect actual &&
+    printf "This is not a translation string"  > expect
+	gettext "This is not a translation string" > actual &&
+    test_cmp expect actual
+'
+
+test_done
-- 
1.7.1.248.gb7713.dirty
Johannes Sixt· Jun 2, 2010, 06:32 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH/RFC v3 6/7] gettext: Basic sanity tests for Git's Gettext support

[When you resend the series in such a manner that you collect a Cc list manually, please do *not* include me. I'm not interested in this series.]

Am 6/2/2010 1:39, schrieb Ævar Arnfjörð Bjarmason:
Show 11 quoted lines
> +test_expect_success 'sanity: $TEXTDOMAIN is git' '
> +	test $TEXTDOMAIN = "git"
> +'
> +
> +test_expect_success 'sanity: $TEXTDOMAINDIR exists' '
> +	test -d $TEXTDOMAINDIR 
> +'
> +
> +test_expect_success 'sanity: Icelandic locale was compiled' '
> +    test -f $TEXTDOMAINDIR/is/LC_MESSAGES/git.mo
> +'
3 times: put "$foo" in double-quotes.
Show 6 quoted lines
> +test_expect_success 'sanity: gettext(unknown) is passed through' '
> +    gettext "" > expect &&
> +    > actual &&
> +    test_cmp expect actual &&
> +    printf "This is not a translation string"  > expect
> +	gettext "This is not a translation string" > actual &&
Strange indentation and you break the && chain.
-- Hannes
Ævar Arnfjörð Bjarmason· Jun 1, 2010, 23:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH/RFC v3 7/7] gettext: Add a skeleton po/is.po

This example is.po file translates a little bit of C, Shell and Perl as an example.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 po/is.po |  282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 282 insertions(+), 0 deletions(-)
 create mode 100644 po/is.po
Show changes to po/is.po +282 −0
diff --git a/po/is.po b/po/is.po
new file mode 100644
index 0000000..aa1923e
--- /dev/null
+++ b/po/is.po
@@ -0,0 +1,282 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Git\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-05-30 19:24+0000\n"
+"PO-Revision-Date: 2010-06-01 22:15+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"
+
+#: wt-status.c:53
+msgid "# Unmerged paths:"
+msgstr ""
+
+#: wt-status.c:59 wt-status.c:76
+#, c-format
+msgid "#   (use \"git reset %s <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:61 wt-status.c:78
+msgid "#   (use \"git rm --cached <file>...\" to unstage)"
+msgstr ""
+
+#: wt-status.c:62
+msgid "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)"
+msgstr ""
+
+#: wt-status.c:63 wt-status.c:79 wt-status.c:98 wt-status.c:110
+#: wt-status.c:115 wt-status.c:638 wt-status.c:640
+msgid "#"
+msgstr ""
+
+#: wt-status.c:70
+msgid "# Changes to be committed:"
+msgstr ""
+
+#: wt-status.c:88
+msgid "# Changed but not updated:"
+msgstr ""
+
+#: wt-status.c:92
+msgid "#   (use \"git add <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:94
+msgid "#   (use \"git add/rm <file>...\" to update what will be committed)"
+msgstr ""
+
+#: wt-status.c:95
+msgid ""
+"#   (use \"git checkout -- <file>...\" to discard changes in working "
+"directory)"
+msgstr ""
+
+#: wt-status.c:97
+msgid "#   (commit or discard the untracked or modified content in submodules)"
+msgstr ""
+
+#: wt-status.c:106
+#, c-format
+msgid "# %s files:"
+msgstr ""
+
+#: wt-status.c:109
+#, c-format
+msgid "#   (use \"git %s <file>...\" to include in what will be committed)"
+msgstr ""
+
+#: wt-status.c:126
+msgid "bug"
+msgstr ""
+
+#: wt-status.c:129 wt-status.c:182
+msgid "#\t"
+msgstr ""
+
+#: wt-status.c:131
+msgid "both deleted:"
+msgstr ""
+
+#: wt-status.c:132
+msgid "added by us:"
+msgstr ""
+
+#: wt-status.c:133
+msgid "deleted by them:"
+msgstr ""
+
+#: wt-status.c:134
+msgid "added by them:"
+msgstr ""
+
+#: wt-status.c:135
+msgid "deleted by us:"
+msgstr ""
+
+#: wt-status.c:136
+msgid "both added:"
+msgstr ""
+
+#: wt-status.c:137
+msgid "both modified:"
+msgstr ""
+
+#: wt-status.c:139
+#, c-format
+msgid "%-20s%s\n"
+msgstr ""
+
+#: wt-status.c:165
+msgid " ("
+msgstr ""
+
+#: wt-status.c:167
+msgid "new commits, "
+msgstr ""
+
+#: wt-status.c:169
+msgid "modified content, "
+msgstr ""
+
+#: wt-status.c:171
+msgid "untracked content, "
+msgstr ""
+
+#: wt-status.c:185
+#, c-format
+msgid "new file:   %s"
+msgstr ""
+
+#: wt-status.c:188
+#, c-format
+msgid "copied:     %s -> %s"
+msgstr ""
+
+#: wt-status.c:191
+#, c-format
+msgid "deleted:    %s"
+msgstr ""
+
+#: wt-status.c:194
+#, c-format
+msgid "modified:   %s"
+msgstr ""
+
+#: wt-status.c:197
+#, c-format
+msgid "renamed:    %s -> %s"
+msgstr ""
+
+#: wt-status.c:200
+#, c-format
+msgid "typechange: %s"
+msgstr ""
+
+#: wt-status.c:203
+#, c-format
+msgid "unknown:    %s"
+msgstr ""
+
+#: wt-status.c:206
+#, c-format
+msgid "unmerged:   %s"
+msgstr ""
+
+#: wt-status.c:209
+#, c-format
+msgid "bug: unhandled diff status %c"
+msgstr ""
+
+#: wt-status.c:212
+#, c-format
+msgid "%s"
+msgstr ""
+
+#: wt-status.c:215
+#, c-format
+msgid "\n"
+msgstr ""
+
+#: wt-status.c:622
+msgid "On branch "
+msgstr "Á greininni "
+
+#: wt-status.c:629
+msgid "Not currently on any branch."
+msgstr "Ekki á neinni grein."
+
+#: wt-status.c:639
+msgid "# Initial commit"
+msgstr ""
+
+#: wt-status.c:651
+msgid "Untracked"
+msgstr ""
+
+#: wt-status.c:651
+msgid "add"
+msgstr ""
+
+#: wt-status.c:653
+msgid "Ignored"
+msgstr ""
+
+#: wt-status.c:653
+msgid "add -f"
+msgstr ""
+
+#: wt-status.c:655
+#, c-format
+msgid "# Untracked files not listed%s\n"
+msgstr ""
+
+#: wt-status.c:657
+msgid " (use -u option to show untracked files)"
+msgstr ""
+
+#: wt-status.c:663
+#, c-format
+msgid "# No changes\n"
+msgstr "# Engar breytingar\n"
+
+#: wt-status.c:667
+#, c-format
+msgid "no changes added to commit%s\n"
+msgstr ""
+
+#: wt-status.c:669
+msgid " (use \"git add\" and/or \"git commit -a\")"
+msgstr ""
+
+#: wt-status.c:671
+#, c-format
+msgid "nothing added to commit but untracked files present%s\n"
+msgstr ""
+
+#: wt-status.c:673
+msgid " (use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:676
+msgid " (create/copy files and use \"git add\" to track)"
+msgstr ""
+
+#: wt-status.c:678 wt-status.c:681
+#, c-format
+msgid "nothing to commit%s\n"
+msgstr ""
+
+#: wt-status.c:679
+msgid " (use -u to show untracked files)"
+msgstr ""
+
+#: wt-status.c:682
+msgid " (working directory clean)"
+msgstr ""
+
+#: git-pull.sh:124
+msgid "Fetching tags only, you probably meant:"
+msgstr "Næ aðeins í tögg, þú áttir líkast til við:"
+
+#: git-pull.sh:125
+msgid "  git fetch --tags"
+msgstr ""
+
+#: git-pull.sh:157
+msgid ""
+"You are not currently on a branch, so I cannot use any\n"
+"'branch.<branchname>.merge' in your configuration file.\n"
+"Please specify which remote branch you want to use on the command\n"
+"line and try again (e.g. 'git pull <repository> <refspec>').\n"
+"See git-pull(1) for details."
+msgstr ""
+
+#: git-send-email.perl:678
+#, perl-format
+msgid "Emails will be sent from: %s\n"
+msgstr "Póstarnir verða sendir frá: %s\n"
-- 
1.7.1.248.gb7713.dirty

← back to recent threads