diff --git a/Documentation/CodingGuidelines b/Documentation/CodingGuidelines index fe1c1e5bc..483008699 100644 --- a/Documentation/CodingGuidelines +++ b/Documentation/CodingGuidelines @@ -81,6 +81,10 @@ For shell scripts specifically (not exhaustive): are ERE elements not BRE (note that \? and \+ are not even part of BRE -- making them accessible from BRE is a GNU extension). + - Use Git's gettext wrappers in git-sh-i18n to make the user + interface translatable. See "Marking strings for translation" in + po/README. + For C programs: - We use tabs to indent, and interpret tabs as taking up to @@ -144,6 +148,9 @@ For C programs: - When we pass pair to functions, we should try to pass them in that order. + - Use Git's gettext wrappers to make the user interface + translatable. See "Marking strings for translation" in po/README. + Writing Documentation: Every user-visible change should be reflected in the documentation. diff --git a/INSTALL b/INSTALL index bf0d97ef7..8120641a5 100644 --- a/INSTALL +++ b/INSTALL @@ -106,6 +106,18 @@ Issues of note: history graphically, and in git-gui. If you don't want gitk or git-gui, you can use NO_TCLTK. + - A gettext library is used by default for localizing Git. The + primary target is GNU libintl, but the Solaris gettext + implementation also works. + + We need a gettext.h on the system for C code, gettext.sh (or + Solaris gettext(1)) for shell scripts, and libintl-perl for Perl + programs. + + Set NO_GETTEXT to disable localization support and make Git only + use English. Under autoconf the configure script will do this + automatically if it can't find libintl on the system. + - Some platform specific issues are dealt with Makefile rules, but depending on your specific installation, you may not have all the libraries/tools needed, or you may have diff --git a/Makefile b/Makefile index 898278d58..9470a1034 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,22 @@ all:: # Define EXPATDIR=/foo/bar if your expat header and library files are in # /foo/bar/include and /foo/bar/lib directories. # +# Define NO_GETTEXT if you don't want Git output to be translated. +# A translated Git requires GNU libintl or another gettext implementation, +# plus libintl-perl at runtime. +# +# Define HAVE_LIBCHARSET_H if you haven't set NO_GETTEXT and you can't +# trust the langinfo.h's nl_langinfo(CODESET) function to return the +# current character set. GNU and Solaris have a nl_langinfo(CODESET), +# FreeBSD can use either, but MinGW and some others need to use +# libcharset.h's locale_charset() instead. +# +# Define LIBC_CONTAINS_LIBINTL if your gettext implementation doesn't +# need -lintl when linking. +# +# Define NO_MSGFMT_EXTENDED_OPTIONS if your implementation of msgfmt +# doesn't support GNU extensions like --check and --statistics +# # Define HAVE_PATHS_H if you have paths.h and want to use the default PATH # it specifies. # @@ -309,6 +325,7 @@ gitexecdir = libexec/git-core mergetoolsdir = $(gitexecdir)/mergetools sharedir = $(prefix)/share gitwebdir = $(sharedir)/gitweb +localedir = $(sharedir)/locale template_dir = share/git-core/templates htmldir = share/doc/git-doc ETC_GITCONFIG = $(sysconfdir)/gitconfig @@ -317,7 +334,7 @@ lib = lib # DESTDIR= pathsep = : -export prefix bindir sharedir sysconfdir gitwebdir +export prefix bindir sharedir sysconfdir gitwebdir localedir CC = gcc AR = ar @@ -330,6 +347,7 @@ RPMBUILD = rpmbuild TCL_PATH = tclsh TCLTK_PATH = wish XGETTEXT = xgettext +MSGFMT = msgfmt PTHREAD_LIBS = -lpthread PTHREAD_CFLAGS = GCOV = gcov @@ -640,6 +658,7 @@ LIB_OBJS += environment.o LIB_OBJS += exec_cmd.o LIB_OBJS += fsck.o LIB_OBJS += gpg-interface.o +LIB_OBJS += gettext.o LIB_OBJS += graph.o LIB_OBJS += grep.o LIB_OBJS += hash.o @@ -836,12 +855,14 @@ ifeq ($(uname_S),Linux) NO_STRLCPY = YesPlease NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease + LIBC_CONTAINS_LIBINTL = YesPlease endif ifeq ($(uname_S),GNU/kFreeBSD) NO_STRLCPY = YesPlease NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease DIR_HAS_BSD_GROUP_SEMANTICS = YesPlease + LIBC_CONTAINS_LIBINTL = YesPlease endif ifeq ($(uname_S),UnixWare) CC = cc @@ -908,6 +929,7 @@ ifeq ($(uname_S),SunOS) NO_MKSTEMPS = YesPlease NO_REGEX = YesPlease NO_FNMATCH_CASEFOLD = YesPlease + NO_MSGFMT_EXTENDED_OPTIONS = YesPlease ifeq ($(uname_R),5.6) SOCKLEN_T = int NO_HSTRERROR = YesPlease @@ -1031,6 +1053,7 @@ ifeq ($(uname_S),GNU) NO_STRLCPY=YesPlease NO_MKSTEMPS = YesPlease HAVE_PATHS_H = YesPlease + LIBC_CONTAINS_LIBINTL = YesPlease endif ifeq ($(uname_S),IRIX) NO_SETENV = YesPlease @@ -1249,6 +1272,7 @@ ifneq (,$(wildcard ../THIS_IS_MSYSGIT)) EXTLIBS += /mingw/lib/libz.a NO_R_TO_GCC_LINKER = YesPlease INTERNAL_QSORT = YesPlease + HAVE_LIBCHARSET_H = YesPlease else NO_CURL = YesPlease endif @@ -1437,6 +1461,11 @@ endif ifdef NEEDS_LIBGEN EXTLIBS += -lgen endif +ifndef NO_GETTEXT +ifndef LIBC_CONTAINS_LIBINTL + EXTLIBS += -lintl +endif +endif ifdef NEEDS_SOCKET EXTLIBS += -lsocket endif @@ -1479,9 +1508,11 @@ ifdef NO_SYMLINK_HEAD BASIC_CFLAGS += -DNO_SYMLINK_HEAD endif ifdef GETTEXT_POISON - LIB_OBJS += gettext.o BASIC_CFLAGS += -DGETTEXT_POISON endif +ifdef NO_GETTEXT + BASIC_CFLAGS += -DNO_GETTEXT +endif ifdef NO_STRCASESTR COMPAT_CFLAGS += -DNO_STRCASESTR COMPAT_OBJS += compat/strcasestr.o @@ -1650,6 +1681,10 @@ ifdef HAVE_PATHS_H BASIC_CFLAGS += -DHAVE_PATHS_H endif +ifdef HAVE_LIBCHARSET_H + BASIC_CFLAGS += -DHAVE_LIBCHARSET_H +endif + ifdef DIR_HAS_BSD_GROUP_SEMANTICS COMPAT_CFLAGS += -DDIR_HAS_BSD_GROUP_SEMANTICS endif @@ -1670,6 +1705,10 @@ ifdef GIT_TEST_CMP_USE_COPIED_CONTEXT export GIT_TEST_CMP_USE_COPIED_CONTEXT endif +ifndef NO_MSGFMT_EXTENDED_OPTIONS + MSGFMT += --check --statistics +endif + ifeq ($(TCLTK_PATH),) NO_TCLTK=NoThanks endif @@ -1700,6 +1739,7 @@ ifndef V QUIET_GEN = @echo ' ' GEN $@; QUIET_LNCP = @echo ' ' LN/CP $@; QUIET_XGETTEXT = @echo ' ' XGETTEXT $@; + QUIET_MSGFMT = @echo ' ' MSGFMT $@; QUIET_GCOV = @echo ' ' GCOV $@; QUIET_SP = @echo ' ' SP $<; QUIET_SUBDIR0 = +@subdir= @@ -1726,6 +1766,7 @@ bindir_SQ = $(subst ','\'',$(bindir)) bindir_relative_SQ = $(subst ','\'',$(bindir_relative)) mandir_SQ = $(subst ','\'',$(mandir)) infodir_SQ = $(subst ','\'',$(infodir)) +localedir_SQ = $(subst ','\'',$(localedir)) gitexecdir_SQ = $(subst ','\'',$(gitexecdir)) template_dir_SQ = $(subst ','\'',$(template_dir)) htmldir_SQ = $(subst ','\'',$(htmldir)) @@ -1781,7 +1822,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 @@ -1831,6 +1872,7 @@ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ -e 's|@SHELL_PATH@|$(SHELL_PATH_SQ)|' \ -e 's|@@DIFF@@|$(DIFF_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 >$@+ @@ -2083,6 +2125,9 @@ config.sp config.s config.o: EXTRA_CPPFLAGS = \ attr.sp attr.s attr.o: EXTRA_CPPFLAGS = \ -DETC_GITATTRIBUTES='"$(ETC_GITATTRIBUTES_SQ)"' +gettext.sp gettext.s gettext.o: EXTRA_CPPFLAGS = \ + -DGIT_LOCALE_PATH='"$(localedir_SQ)"' + http.sp http.s http.o: EXTRA_CPPFLAGS = \ -DGIT_HTTP_USER_AGENT='"git/$(GIT_VERSION)"' @@ -2156,17 +2201,37 @@ XGETTEXT_FLAGS = \ XGETTEXT_FLAGS_C = $(XGETTEXT_FLAGS) --language=C \ --keyword=_ --keyword=N_ --keyword="Q_:1,2" XGETTEXT_FLAGS_SH = $(XGETTEXT_FLAGS) --language=Shell +XGETTEXT_FLAGS_PERL = $(XGETTEXT_FLAGS) --keyword=__ --language=Perl LOCALIZED_C := $(C_OBJ:o=c) LOCALIZED_SH := $(SCRIPT_SH) +LOCALIZED_PERL := $(SCRIPT_PERL) + +ifdef XGETTEXT_INCLUDE_TESTS +LOCALIZED_C += t/t0200/test.c +LOCALIZED_SH += t/t0200/test.sh +LOCALIZED_PERL += t/t0200/test.perl +endif po/git.pot: $(LOCALIZED_C) $(QUIET_XGETTEXT)$(XGETTEXT) -o$@+ $(XGETTEXT_FLAGS_C) $(LOCALIZED_C) $(QUIET_XGETTEXT)$(XGETTEXT) -o$@+ --join-existing $(XGETTEXT_FLAGS_SH) \ $(LOCALIZED_SH) + $(QUIET_XGETTEXT)$(XGETTEXT) -o$@+ --join-existing $(XGETTEXT_FLAGS_PERL) \ + $(LOCALIZED_PERL) mv $@+ $@ pot: po/git.pot +POFILES := $(wildcard po/*.po) +MOFILES := $(patsubst po/%.po,po/build/locale/%/LC_MESSAGES/git.mo,$(POFILES)) + +ifndef NO_GETTEXT +all:: $(MOFILES) +endif + +po/build/locale/%/LC_MESSAGES/git.mo: po/%.po + $(QUIET_MSGFMT)mkdir -p $(dir $@) && $(MSGFMT) -o $@ $< + FIND_SOURCE_FILES = ( git ls-files '*.[hcS]' 2>/dev/null || \ $(FIND) . \( -name .git -type d -prune \) \ -o \( -name '*.[hcS]' -type f -print \) ) @@ -2185,7 +2250,8 @@ cscope: ### Detect prefix changes TRACK_CFLAGS = $(CC):$(subst ','\'',$(ALL_CFLAGS)):\ - $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ) + $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ):\ + $(localedir_SQ) GIT-CFLAGS: FORCE @FLAGS='$(TRACK_CFLAGS)'; \ @@ -2222,6 +2288,7 @@ endif ifdef GIT_TEST_CMP_USE_COPIED_CONTEXT @echo GIT_TEST_CMP_USE_COPIED_CONTEXT=YesPlease >>$@ endif + @echo NO_GETTEXT=\''$(subst ','\'',$(subst ','\'',$(NO_GETTEXT)))'\' >>$@ @echo GETTEXT_POISON=\''$(subst ','\'',$(subst ','\'',$(GETTEXT_POISON)))'\' >>$@ @echo NO_UNIX_SOCKETS=\''$(subst ','\'',$(subst ','\'',$(NO_UNIX_SOCKETS)))'\' >>$@ @@ -2338,6 +2405,11 @@ install: all $(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(mergetools_instdir_SQ)' $(INSTALL) -m 644 mergetools/* '$(DESTDIR_SQ)$(mergetools_instdir_SQ)' +ifndef NO_GETTEXT + $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(localedir_SQ)' + (cd po/build/locale && $(TAR) cf - .) | \ + (cd '$(DESTDIR_SQ)$(localedir_SQ)' && umask 022 && $(TAR) xof -) +endif ifndef NO_PERL $(MAKE) -C perl prefix='$(prefix_SQ)' DESTDIR='$(DESTDIR_SQ)' install $(MAKE) -C gitweb install @@ -2474,6 +2546,7 @@ clean: $(RM) $(TEST_PROGRAMS) $(RM) -r bin-wrappers $(RM) -r $(dep_dirs) + $(RM) -r po/build/ $(RM) *.spec *.pyc *.pyo */*.pyc */*.pyo common-cmds.h $(ETAGS_TARGET) tags cscope* $(RM) -r autom4te.cache $(RM) config.log config.mak.autogen config.mak.append config.status config.cache diff --git a/config.mak.in b/config.mak.in index ab371012a..10698c829 100644 --- a/config.mak.in +++ b/config.mak.in @@ -35,6 +35,9 @@ NO_CURL=@NO_CURL@ NO_EXPAT=@NO_EXPAT@ NO_LIBGEN_H=@NO_LIBGEN_H@ HAVE_PATHS_H=@HAVE_PATHS_H@ +HAVE_LIBCHARSET_H=@HAVE_LIBCHARSET_H@ +NO_GETTEXT=@NO_GETTEXT@ +LIBC_CONTAINS_LIBINTL=@LIBC_CONTAINS_LIBINTL@ NEEDS_LIBICONV=@NEEDS_LIBICONV@ NEEDS_SOCKET=@NEEDS_SOCKET@ NEEDS_RESOLV=@NEEDS_RESOLV@ diff --git a/configure.ac b/configure.ac index 048a1d497..630dbdd19 100644 --- a/configure.ac +++ b/configure.ac @@ -636,6 +636,12 @@ AC_CHECK_LIB([c], [basename], AC_SUBST(NEEDS_LIBGEN) test -n "$NEEDS_LIBGEN" && LIBS="$LIBS -lgen" +AC_CHECK_LIB([c], [gettext], +[LIBC_CONTAINS_LIBINTL=YesPlease], +[LIBC_CONTAINS_LIBINTL=]) +AC_SUBST(LIBC_CONTAINS_LIBINTL) +test -n "$LIBC_CONTAINS_LIBINTL" || LIBS="$LIBS -lintl" + ## Checks for header files. AC_MSG_NOTICE([CHECKS for header files]) # @@ -818,6 +824,19 @@ AC_CHECK_HEADER([paths.h], [HAVE_PATHS_H=]) AC_SUBST(HAVE_PATHS_H) # +# Define NO_GETTEXT if you don't want Git output to be translated. +# A translated Git requires GNU libintl or another gettext implementation +AC_CHECK_HEADER([libintl.h], +[NO_GETTEXT=], +[NO_GETTEXT=YesPlease]) +AC_SUBST(NO_GETTEXT) +# +# Define HAVE_LIBCHARSET_H if have libcharset.h +AC_CHECK_HEADER([libcharset.h], +[HAVE_LIBCHARSET_H=YesPlease], +[HAVE_LIBCHARSET_H=]) +AC_SUBST(HAVE_LIBCHARSET_H) +# # Define NO_STRCASESTR if you don't have strcasestr. GIT_CHECK_FUNC(strcasestr, [NO_STRCASESTR=], diff --git a/daemon.c b/daemon.c index fa283003e..15ce918a2 100644 --- a/daemon.c +++ b/daemon.c @@ -1099,6 +1099,8 @@ int main(int argc, char **argv) struct credentials *cred = NULL; int i; + git_setup_gettext(); + git_extract_argv0_path(argv[0]); for (i = 1; i < argc; i++) { diff --git a/fast-import.c b/fast-import.c index 350b2e9e1..4b9c4b73a 100644 --- a/fast-import.c +++ b/fast-import.c @@ -3305,6 +3305,8 @@ int main(int argc, const char **argv) git_extract_argv0_path(argv[0]); + git_setup_gettext(); + if (argc == 2 && !strcmp(argv[1], "-h")) usage(fast_import_usage); diff --git a/gettext.c b/gettext.c index ae5394a49..f75bca7f5 100644 --- a/gettext.c +++ b/gettext.c @@ -5,6 +5,18 @@ #include "git-compat-util.h" #include "gettext.h" +#ifndef NO_GETTEXT +# include +# include +# ifdef HAVE_LIBCHARSET_H +# include +# else +# include +# define locale_charset() nl_langinfo(CODESET) +# endif +#endif + +#ifdef GETTEXT_POISON int use_gettext_poison(void) { static int poison_requested = -1; @@ -12,3 +24,108 @@ int use_gettext_poison(void) poison_requested = getenv("GIT_GETTEXT_POISON") ? 1 : 0; return poison_requested; } +#endif + +#ifndef NO_GETTEXT +static void init_gettext_charset(const char *domain) +{ + const char *charset; + + /* + This trick arranges for messages to be emitted in the user's + requested encoding, but avoids setting LC_CTYPE from the + environment for the whole program. + + This primarily done to avoid a bug in vsnprintf in the GNU C + Library [1]. which triggered a "your vsnprintf is broken" error + on Git's own repository when inspecting v0.99.6~1 under a UTF-8 + locale. + + That commit contains a ISO-8859-1 encoded author name, which + the locale aware vsnprintf(3) won't interpolate in the format + argument, due to mismatch between the data encoding and the + locale. + + Even if it wasn't for that bug we wouldn't want to use LC_CTYPE at + this point, because it'd require auditing all the code that uses C + functions whose semantics are modified by LC_CTYPE. + + But only setting LC_MESSAGES as we do creates a problem, since + we declare the encoding of our PO files[2] the gettext + implementation will try to recode it to the user's locale, but + without LC_CTYPE it'll emit something like this on 'git init' + under the Icelandic locale: + + Bj? til t?ma Git lind ? /hlagh/.git/ + + Gettext knows about the encoding of our PO file, but we haven't + told it about the user's encoding, so all the non-US-ASCII + characters get encoded to question marks. + + But we're in luck! We can set LC_CTYPE from the environment + only while we call nl_langinfo and + bind_textdomain_codeset. That suffices to tell gettext what + encoding it should emit in, so it'll now say: + + Bjó til tóma Git lind í /hlagh/.git/ + + And the equivalent ISO-8859-1 string will be emitted under a + ISO-8859-1 locale. + + With this change way we get the advantages of setting LC_CTYPE + (talk to the user in his language/encoding), without the major + drawbacks (changed semantics for C functions we rely on). + + However foreign functions using other message catalogs that + aren't using our neat trick will still have a problem, e.g. if + we have to call perror(3): + + #include + #include + #include + + int main(void) + { + setlocale(LC_MESSAGES, ""); + setlocale(LC_CTYPE, "C"); + errno = ENODEV; + perror("test"); + return 0; + } + + Running that will give you a message with question marks: + + $ LANGUAGE= LANG=de_DE.utf8 ./test + test: Kein passendes Ger?t gefunden + + In the long term we should probably see about getting that + vsnprintf bug in glibc fixed, and audit our code so it won't + fall apart under a non-C locale. + + Then we could simply set LC_CTYPE from the environment, which would + make things like the external perror(3) messages work. + + See t/t0203-gettext-setlocale-sanity.sh's "gettext.c" tests for + regression tests. + + 1. http://sourceware.org/bugzilla/show_bug.cgi?id=6530 + 2. E.g. "Content-Type: text/plain; charset=UTF-8\n" in po/is.po + */ + setlocale(LC_CTYPE, ""); + charset = locale_charset(); + bind_textdomain_codeset(domain, charset); + setlocale(LC_CTYPE, "C"); +} + +void git_setup_gettext(void) +{ + const char *podir = getenv("GIT_TEXTDOMAINDIR"); + + if (!podir) + podir = GIT_LOCALE_PATH; + bindtextdomain("git", podir); + setlocale(LC_MESSAGES, ""); + init_gettext_charset("git"); + textdomain("git"); +} +#endif diff --git a/gettext.h b/gettext.h index 24d91824e..57ba8bb02 100644 --- a/gettext.h +++ b/gettext.h @@ -13,8 +13,29 @@ #error "namespace conflict: '_' or 'Q_' is pre-defined?" #endif +#ifndef NO_GETTEXT +# include +#else +# ifdef gettext +# undef gettext +# endif +# define gettext(s) (s) +# ifdef ngettext +# undef ngettext +# endif +# define ngettext(s, p, n) ((n == 1) ? (s) : (p)) +#endif + #define FORMAT_PRESERVING(n) __attribute__((format_arg(n))) +#ifndef NO_GETTEXT +extern void git_setup_gettext(void); +#else +static inline void git_setup_gettext(void) +{ +} +#endif + #ifdef GETTEXT_POISON extern int use_gettext_poison(void); #else @@ -23,7 +44,7 @@ extern int use_gettext_poison(void); static inline FORMAT_PRESERVING(1) const char *_(const char *msgid) { - return use_gettext_poison() ? "# GETTEXT POISON #" : msgid; + return use_gettext_poison() ? "# GETTEXT POISON #" : gettext(msgid); } static inline FORMAT_PRESERVING(1) FORMAT_PRESERVING(2) @@ -31,7 +52,7 @@ const char *Q_(const char *msgid, const char *plu, unsigned long n) { if (use_gettext_poison()) return "# GETTEXT POISON #"; - return n == 1 ? msgid : plu; + return ngettext(msgid, plu, n); } /* Mark msgid for translation but do not translate it. */ diff --git a/git-sh-i18n.sh b/git-sh-i18n.sh index e672366f0..b4575fb3a 100644 --- a/git-sh-i18n.sh +++ b/git-sh-i18n.sh @@ -2,47 +2,91 @@ # # Copyright (c) 2010 Ævar Arnfjörð Bjarmason # -# This is a skeleton no-op implementation of gettext for Git. It'll be -# replaced by something that uses gettext.sh in a future patch series. +# This is Git's interface to gettext.sh. See po/README for usage +# instructions. + +# Export the TEXTDOMAIN* data that we need for Git +TEXTDOMAIN=git +export TEXTDOMAIN +if test -z "$GIT_TEXTDOMAINDIR" +then + TEXTDOMAINDIR="@@LOCALEDIR@@" +else + TEXTDOMAINDIR="$GIT_TEXTDOMAINDIR" +fi +export TEXTDOMAINDIR if test -z "$GIT_GETTEXT_POISON" then - gettext () { - printf "%s" "$1" - } + if test -z "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" && type gettext.sh >/dev/null 2>&1 + then + # This is GNU libintl's gettext.sh, we don't need to do anything + # else than setting up the environment and loading gettext.sh + GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu + export GIT_INTERNAL_GETTEXT_SH_SCHEME - gettextln() { - printf "%s\n" "$1" - } + # Try to use libintl's gettext.sh, or fall back to English if we + # can't. + . gettext.sh - eval_gettext () { - printf "%s" "$1" | ( - export PATH $(git sh-i18n--envsubst --variables "$1"); - git sh-i18n--envsubst "$1" - ) - } + elif test -z "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" && test "$(gettext -h 2>&1)" = "-h" + then + # We don't have gettext.sh, but there's a gettext binary in our + # path. This is probably Solaris or something like it which has a + # gettext implementation that isn't GNU libintl. + GIT_INTERNAL_GETTEXT_SH_SCHEME=solaris + export GIT_INTERNAL_GETTEXT_SH_SCHEME - eval_gettextln () { - printf "%s\n" "$1" | ( - export PATH $(git sh-i18n--envsubst --variables "$1"); - git sh-i18n--envsubst "$1" - ) - } + # Solaris has a gettext(1) but no eval_gettext(1) + eval_gettext () { + gettext "$1" | ( + export PATH $(git sh-i18n--envsubst --variables "$1"); + git sh-i18n--envsubst "$1" + ) + } + + else + # Since gettext.sh isn't available we'll have to define our own + # dummy pass-through functions. + + # Tell our tests that we don't have the real gettext.sh + GIT_INTERNAL_GETTEXT_SH_SCHEME=fallthrough + export GIT_INTERNAL_GETTEXT_SH_SCHEME + + gettext () { + printf "%s" "$1" + } + + eval_gettext () { + printf "%s" "$1" | ( + export PATH $(git sh-i18n--envsubst --variables "$1"); + git sh-i18n--envsubst "$1" + ) + } + fi else + # Emit garbage under GETTEXT_POISON=YesPlease. Unlike the C tests + # this relies on an environment variable + + GIT_INTERNAL_GETTEXT_SH_SCHEME=poison + export GIT_INTERNAL_GETTEXT_SH_SCHEME + gettext () { printf "%s" "# GETTEXT POISON #" } - gettextln () { - printf "%s\n" "# GETTEXT POISON #" - } - eval_gettext () { printf "%s" "# GETTEXT POISON #" } - - eval_gettextln () { - printf "%s\n" "# GETTEXT POISON #" - } fi +# Git-specific wrapper functions +gettextln () { + gettext "$1" + echo +} + +eval_gettextln () { + eval_gettext "$1" + echo +} diff --git a/git.c b/git.c index 250f4483e..fb9029cbf 100644 --- a/git.c +++ b/git.c @@ -538,6 +538,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/http-backend.c b/http-backend.c index 59ad7da60..869d51538 100644 --- a/http-backend.c +++ b/http-backend.c @@ -545,6 +545,8 @@ int main(int argc, char **argv) char *cmd_arg = NULL; int i; + git_setup_gettext(); + git_extract_argv0_path(argv[0]); set_die_routine(die_webcgi); diff --git a/http-fetch.c b/http-fetch.c index 94d47cbb2..ba3ea1067 100644 --- a/http-fetch.c +++ b/http-fetch.c @@ -22,6 +22,8 @@ int main(int argc, const char **argv) int get_verbosely = 0; int get_recover = 0; + git_setup_gettext(); + git_extract_argv0_path(argv[0]); while (arg < argc && argv[arg][0] == '-') { diff --git a/http-push.c b/http-push.c index cdfdd4f79..f22f7e43c 100644 --- a/http-push.c +++ b/http-push.c @@ -1748,6 +1748,8 @@ int main(int argc, char **argv) int new_refs; struct ref *ref, *local_refs; + git_setup_gettext(); + git_extract_argv0_path(argv[0]); repo = xcalloc(sizeof(*repo), 1); diff --git a/imap-send.c b/imap-send.c index 80e0e8c05..91763d301 100644 --- a/imap-send.c +++ b/imap-send.c @@ -1538,6 +1538,8 @@ int main(int argc, char **argv) git_extract_argv0_path(argv[0]); + git_setup_gettext(); + if (argc != 1) usage(imap_send_usage); diff --git a/perl/Git/I18N.pm b/perl/Git/I18N.pm new file mode 100644 index 000000000..07597dcb9 --- /dev/null +++ b/perl/Git/I18N.pm @@ -0,0 +1,89 @@ +package Git::I18N; +use 5.008; +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT = qw(__); +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 +{ + # Used by our test script to see if it should test fallbacks or + # not. + our $__HAS_LIBRARY = 1; + + local $@; + eval { + __bootstrap_locale_messages(); + *__ = \&Locale::Messages::gettext; + 1; + } or do { + # Tell test.pl that we couldn't load the gettext library. + $Git::I18N::__HAS_LIBRARY = 0; + + # Just a fall-through no-op + *__ = sub ($) { $_[0] }; + }; +} + +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 Perl interface to gettext via L. If +L can't be loaded (it's not a core module) we +provide stub passthrough fallbacks. + +This is a distilled interface to gettext, see C +for the full interface. This module implements only a small part of +it. + +=head1 FUNCTIONS + +=head2 __($) + +L's gettext function if all goes well, otherwise our +passthrough fallback function. + +=head1 AUTHOR + +Evar ArnfjErE Bjarmason + +=head1 COPYRIGHT + +Copyright 2010 Evar ArnfjErE Bjarmason + +=cut diff --git a/perl/Makefile b/perl/Makefile index a2ffb6402..b2977cd0b 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)' INSTALL_BASE='' + $(PERL_PATH) $< PREFIX='$(prefix_SQ)' INSTALL_BASE='' --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 0b9deca2c..456d45bf4 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 index a242a86e9..4caa631ff 100644 --- a/po/.gitignore +++ b/po/.gitignore @@ -1 +1,2 @@ /git.pot +/build diff --git a/po/README b/po/README new file mode 100644 index 000000000..10b0ad2ce --- /dev/null +++ b/po/README @@ -0,0 +1,229 @@ +Core GIT Translations +===================== + +This directory holds the translations for the core of Git. This +document describes how to add to and maintain these translations, and +how to mark source strings for translation. + + +Generating a .pot file +---------------------- + +The po/git.pot file contains a message catalog extracted from Git's +sources. You need to generate it to add new translations with +msginit(1), or update existing ones with msgmerge(1). + +Since the file can be automatically generated it's not checked into +git.git. To generate it do, at the top-level: + + make pot + + +Initializing a .po file +----------------------- + +To add a new translation first generate git.pot (see above) and then +in the po/ directory do: + + msginit --locale=XX + +Where XX is your locale, e.g. "is", "de" or "pt_BR". + +Then edit the automatically generated copyright info in your new XX.po +to be correct, e.g. for Icelandic: + + @@ -1,6 +1,6 @@ + -# Icelandic translations for PACKAGE package. + -# Copyright (C) 2010 THE PACKAGE'S COPYRIGHT HOLDER + -# This file is distributed under the same license as the PACKAGE package. + +# Icelandic translations for Git. + +# Copyright (C) 2010 Ævar Arnfjörð Bjarmason + +# This file is distributed under the same license as the Git package. + # Ævar Arnfjörð Bjarmason , 2010. + +And change references to PACKAGE VERSION in the PO Header Entry to +just "Git": + + perl -pi -e 's/(?<="Project-Id-Version: )PACKAGE VERSION/Git/' XX.po + + +Updating a .po file +------------------- + +If there's an existing *.po file for your language but you need to +update the translation you first need to generate git.pot (see above) +and then in the po/ directory do: + + msgmerge --add-location --backup=off -U XX.po git.pot + +Where XX.po is the file you want to update. + +Testing your changes +-------------------- + +Before you submit your changes go back to the top-level and do: + + make + +On systems with GNU gettext (i.e. not Solaris) this will compile your +changed PO file with `msgfmt --check`, the --check option flags many +common errors, e.g. missing printf format strings, or translated +messages that deviate from the originals in whether they begin/end +with a newline or not. + + +Marking strings for translation +------------------------------- + +Before strings can be translated they first have to be marked for +translation. + +Git uses an internationalization interface that wraps the system's +gettext library, so most of the advice in your gettext documentation +(on GNU systems `info gettext` in a terminal) applies. + +General advice: + + - Don't mark everything for translation, only strings which will be + read by humans (the porcelain interface) should be translated. + + The output from Git's plumbing utilities will primarily be read by + programs and would break scripts under non-C locales if it was + translated. Plumbing strings should not be translated, since + they're part of Git's API. + + - Adjust the strings so that they're easy to translate. Most of the + advice in `info '(gettext)Preparing Strings'` applies here. + + - If something is unclear or ambiguous you can use a "TRANSLATORS" + comment to tell the translators what to make of it. These will be + extracted by xgettext(1) and put in the po/*.po files, e.g. from + git-am.sh: + + # TRANSLATORS: Make sure to include [y], [n], [e], [v] and [a] + # in your translation. The program will only accept English + # input at this point. + gettext "Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all " + + Or in C, from builtin/revert.c: + + /* TRANSLATORS: %s will be "revert" or "cherry-pick" */ + die(_("%s: Unable to write new index file"), action_name(opts)); + +We provide wrappers for C, Shell and Perl programs. Here's how they're +used: + +C: + + - Include builtin.h at the top, it'll pull in in gettext.h, which + defines the gettext interface. Consult with the list if you need to + use gettext.h directly. + + - The C interface is a subset of the normal GNU gettext + interface. We currently export these functions: + + - _() + + Mark and translate a string. E.g.: + + printf(_("HEAD is now at %s"), hex); + + - Q_() + + Mark and translate a plural string. E.g.: + + printf(Q_("%d commit", "%d commits", number_of_commits)); + + This is just a wrapper for the ngettext() function. + + - N_() + + A no-op pass-through macro for marking strings inside static + initializations, e.g.: + + static const char *reset_type_names[] = { + N_("mixed"), N_("soft"), N_("hard"), N_("merge"), N_("keep"), NULL + }; + + And then, later: + + die(_("%s reset is not allowed in a bare repository"), + _(reset_type_names[reset_type])); + + Here _() couldn't have statically determined what the translation + string will be, but since it was already marked for translation + with N_() the look-up in the message catalog will succeed. + +Shell: + + - The Git gettext shell interface is just a wrapper for + gettext.sh. Import it right after git-sh-setup like this: + + . git-sh-setup + . git-sh-i18n + + And then use the gettext or eval_gettext functions: + + # For constant interface messages: + gettext "A message for the user"; echo + + # To interpolate variables: + details="oh noes" + eval_gettext "An error occured: \$details"; echo + + In addition we have wrappers for messages that end with a trailing + newline. I.e. you could write the above as: + + # For constant interface messages: + gettextln "A message for the user" + + # To interpolate variables: + details="oh noes" + eval_gettextln "An error occured: \$details" + + More documentation about the interface is available in the GNU info + page: `info '(gettext)sh'`. Looking at git-am.sh (the first shell + command to be translated) for examples is also useful: + + git log --reverse -p --grep=i18n git-am.sh + +Perl: + + - The Git::I18N module provides a limited subset of the + Locale::Messages functionality, e.g.: + + use Git::I18N; + print __("Welcome to Git!\n"); + printf __("The following error occured: %s\n"), $error; + + Run `perldoc perl/Git/I18N.pm` for more info. + + +Testing marked strings +---------------------- + +Even if you've correctly marked porcelain strings for translation +something in the test suite might still depend on the US English +version of the strings, e.g. to grep some error message or other +output. + +To smoke out issues like these Git can be compiled with gettext poison +support, at the top-level: + + make GETTEXT_POISON=YesPlease + +That'll give you a git which emits gibberish on every call to +gettext. It's obviously not meant to be installed, but you should run +the test suite with it: + + cd t && prove -j 9 ./t[0-9]*.sh + +If tests break with it you should inspect them manually and see if +what you're translating is sane, i.e. that you're not translating +plumbing output. + +If not you should replace calls to grep with test_i18ngrep, or +test_cmp calls with test_i18ncmp. If that's not enough you can skip +the whole test by making it depend on the C_LOCALE_OUTPUT +prerequisite. See existing test files with this prerequisite for +examples. diff --git a/po/is.po b/po/is.po new file mode 100644 index 000000000..8692a8b5b --- /dev/null +++ b/po/is.po @@ -0,0 +1,93 @@ +# Icelandic translations for Git. +# Copyright (C) 2010 Ævar Arnfjörð Bjarmason +# This file is distributed under the same license as the Git package. +# Ævar Arnfjörð Bjarmason , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: Git\n" +"Report-Msgid-Bugs-To: Git Mailing List \n" +"POT-Creation-Date: 2010-09-20 14:44+0000\n" +"PO-Revision-Date: 2010-06-05 19:06 +0000\n" +"Last-Translator: Ævar Arnfjörð Bjarmason \n" +"Language-Team: Git Mailing List \n" +"Language: is\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.c:5 +msgid "See 'git help COMMAND' for more information on a specific command." +msgstr "Sjá 'git help SKIPUN' til að sjá hjálp fyrir tiltekna skipun." + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.c:10 +msgid "TEST: A C test string" +msgstr "TILRAUN: C tilraunastrengur" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.c:13 +#, 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/test.c:16 +#, c-format +msgid "TEST: Hello World!" +msgstr "TILRAUN: Halló Heimur!" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.c:19 +#, c-format +msgid "TEST: Old English Runes" +msgstr "TILRAUN: ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.c:22 +#, c-format +msgid "TEST: ‘single’ and “double” quotes" +msgstr "TILRAUN: ‚einfaldar‘ og „tvöfaldar“ gæsalappir" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/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/test.sh:11 +#, sh-format +msgid "TEST: A Shell test $variable" +msgstr "TILRAUN: Skeljartilraunastrengur með breytunni $variable" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.perl:8 +msgid "TEST: A Perl test string" +msgstr "TILRAUN: Perl tilraunastrengur" + +#. TRANSLATORS: This is a test. You don't need to translate it. +#: t/t0200/test.perl:11 +#, perl-format +msgid "TEST: A Perl test variable %s" +msgstr "TILRAUN: Perl tilraunastrengur með breytunni %s" + +#. TRANSLATORS: The first '%s' is either "Reinitialized +#. existing" or "Initialized empty", the second " shared" or +#. "", and the last '%s%s' is the verbatim directory name. +#: builtin/init-db.c:355 +#, c-format +msgid "%s%s Git repository in %s%s\n" +msgstr "%s%s Git lind í %s%s\n" + +#: builtin/init-db.c:356 +msgid "Reinitialized existing" +msgstr "Endurgerði" + +#: builtin/init-db.c:356 +msgid "Initialized empty" +msgstr "Bjó til tóma" + +#: builtin/init-db.c:357 +msgid " shared" +msgstr " sameiginlega" diff --git a/shell.c b/shell.c index abb862246..84b237fef 100644 --- a/shell.c +++ b/shell.c @@ -137,6 +137,8 @@ int main(int argc, char **argv) int devnull_fd; int count; + git_setup_gettext(); + git_extract_argv0_path(argv[0]); /* diff --git a/show-index.c b/show-index.c index 63f9da532..5a9eed7fd 100644 --- a/show-index.c +++ b/show-index.c @@ -11,6 +11,8 @@ int main(int argc, char **argv) unsigned int version; static unsigned int top_index[256]; + git_setup_gettext(); + if (argc != 1) usage(show_index_usage); if (fread(top_index, 2 * 4, 1, stdin) != 1) diff --git a/t/lib-gettext.sh b/t/lib-gettext.sh new file mode 100644 index 000000000..0f76f6cdc --- /dev/null +++ b/t/lib-gettext.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason +# + +. ./test-lib.sh + +GIT_TEXTDOMAINDIR="$GIT_BUILD_DIR/po/build/locale" +GIT_PO_PATH="$GIT_BUILD_DIR/po" +export GIT_TEXTDOMAINDIR GIT_PO_PATH + +. "$GIT_BUILD_DIR"/git-sh-i18n + +if test_have_prereq GETTEXT && ! test_have_prereq GETTEXT_POISON +then + # is_IS.UTF-8 on Solaris and FreeBSD, is_IS.utf8 on Debian + is_IS_locale=$(locale -a | sed -n '/^is_IS\.[uU][tT][fF]-*8$/{ + p + q + }') + # is_IS.ISO8859-1 on Solaris and FreeBSD, is_IS.iso88591 on Debian + is_IS_iso_locale=$(locale -a | sed -n '/^is_IS\.[iI][sS][oO]8859-*1$/{ + p + q + }') + + # Export them as an environment variable so the t0202/test.pl Perl + # test can use it too + export is_IS_locale is_IS_iso_locale + + if test -n "$is_IS_locale" && + test $GIT_INTERNAL_GETTEXT_SH_SCHEME != "fallthrough" + then + # Some of the tests need the reference Icelandic locale + test_set_prereq GETTEXT_LOCALE + + # Exporting for t0202/test.pl + GETTEXT_LOCALE=1 + export GETTEXT_LOCALE + say "# lib-gettext: Found '$is_IS_locale' as an is_IS UTF-8 locale" + else + say "# lib-gettext: No is_IS UTF-8 locale available" + fi + + if test -n "$is_IS_iso_locale" && + test $GIT_INTERNAL_GETTEXT_SH_SCHEME != "fallthrough" + then + # Some of the tests need the reference Icelandic locale + test_set_prereq GETTEXT_ISO_LOCALE + + say "# lib-gettext: Found '$is_IS_iso_locale' as an is_IS ISO-8859-1 locale" + else + say "# lib-gettext: No is_IS ISO-8859-1 locale available" + fi +fi diff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh new file mode 100755 index 000000000..8853d8afb --- /dev/null +++ b/t/t0200-gettext-basic.sh @@ -0,0 +1,108 @@ +#!/bin/sh +# +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason +# + +test_description='Gettext support for Git' + +. ./lib-gettext.sh + +test_expect_success "sanity: \$GIT_INTERNAL_GETTEXT_SH_SCHEME is set (to $GIT_INTERNAL_GETTEXT_SH_SCHEME)" ' + test -n "$GIT_INTERNAL_GETTEXT_SH_SCHEME" +' + +test_expect_success 'sanity: $TEXTDOMAIN is git' ' + test $TEXTDOMAIN = "git" +' + +test_expect_success 'xgettext sanity: Perl _() strings are not extracted' ' + ! grep "A Perl string xgettext will not get" "$GIT_PO_PATH"/is.po +' + +test_expect_success 'xgettext sanity: Comment extraction with --add-comments' ' + grep "TRANSLATORS: This is a test" "$TEST_DIRECTORY"/t0200/* | wc -l >expect && + grep "TRANSLATORS: This is a test" "$GIT_PO_PATH"/is.po | wc -l >actual && + test_cmp expect actual +' + +test_expect_success 'xgettext sanity: Comment extraction with --add-comments stops at statements' ' + ! grep "This is a phony" "$GIT_PO_PATH"/is.po && + ! grep "the above comment" "$GIT_PO_PATH"/is.po +' + +test_expect_success GETTEXT 'sanity: $TEXTDOMAINDIR exists without NO_GETTEXT=YesPlease' ' + test -d "$TEXTDOMAINDIR" && + test "$TEXTDOMAINDIR" = "$GIT_TEXTDOMAINDIR" +' + +test_expect_success GETTEXT 'sanity: Icelandic locale was compiled' ' + test -f "$TEXTDOMAINDIR/is/LC_MESSAGES/git.mo" +' + +# 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 GETTEXT_LOCALE 'sanity: gettext("") metadata is OK' ' + # Return value may be non-zero + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "" >zero-expect && + grep "Project-Id-Version: Git" zero-expect && + grep "Git Mailing List " zero-expect && + grep "Content-Type: text/plain; charset=UTF-8" zero-expect && + grep "Content-Transfer-Encoding: 8bit" zero-expect +' + +test_expect_success GETTEXT_LOCALE '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 GETTEXT_LOCALE 'xgettext: C extraction of _() and N_() strings' ' + printf "TILRAUN: C tilraunastrengur" >expect && + printf "\n" >>expect && + printf "Sjá '\''git help SKIPUN'\'' til að sjá hjálp fyrir tiltekna skipun." >>expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: A C test string" >actual && + printf "\n" >>actual && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "See '\''git help COMMAND'\'' for more information on a specific command." >>actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_LOCALE 'xgettext: C extraction with %s' ' + printf "TILRAUN: C tilraunastrengur %%s" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: A C test string %s" >actual && + test_cmp expect actual +' + +# xgettext from Shell +test_expect_success GETTEXT_LOCALE 'xgettext: Shell extraction' ' + printf "TILRAUN: Skeljartilraunastrengur" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: A Shell test string" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_LOCALE 'xgettext: Shell extraction with $variable' ' + printf "TILRAUN: Skeljartilraunastrengur með breytunni a var i able" >x-expect && + LANGUAGE=is LC_ALL="$is_IS_locale" 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 GETTEXT_LOCALE 'xgettext: Perl extraction' ' + printf "TILRAUN: Perl tilraunastrengur" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: A Perl test string" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_LOCALE 'xgettext: Perl extraction with %s' ' + printf "TILRAUN: Perl tilraunastrengur með breytunni %%s" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: A Perl test variable %s" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_LOCALE 'sanity: Some gettext("") data for real locale' ' + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "" >real-locale && + test -s real-locale +' + +test_done diff --git a/t/t0200/test.c b/t/t0200/test.c new file mode 100644 index 000000000..584d45cf3 --- /dev/null +++ b/t/t0200/test.c @@ -0,0 +1,23 @@ +/* This is a phony C program that's only here to test xgettext message extraction */ + +const char help[] = + /* TRANSLATORS: This is a test. You don't need to translate it. */ + N_("See 'git help COMMAND' for more information on a specific command."); + +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"); + + /* TRANSLATORS: This is a test. You don't need to translate it. */ + printf(_("TEST: Hello World!")); + + /* TRANSLATORS: This is a test. You don't need to translate it. */ + printf(_("TEST: Old English Runes")); + + /* TRANSLATORS: This is a test. You don't need to translate it. */ + printf(_("TEST: ‘single’ and “double” quotes")); +} diff --git a/t/t0200/test.perl b/t/t0200/test.perl new file mode 100644 index 000000000..36fba341b --- /dev/null +++ b/t/t0200/test.perl @@ -0,0 +1,14 @@ +# This is a phony Perl program that's only here to test xgettext +# message extraction + +# so the above comment won't be folded into the next one by xgettext +1; + +# 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/test.sh b/t/t0200/test.sh new file mode 100644 index 000000000..022d607f4 --- /dev/null +++ b/t/t0200/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-fallbacks.sh b/t/t0201-gettext-fallbacks.sh index 54d98b9b1..52b1c27c2 100755 --- a/t/t0201-gettext-fallbacks.sh +++ b/t/t0201-gettext-fallbacks.sh @@ -5,8 +5,24 @@ test_description='Gettext Shell fallbacks' -. ./test-lib.sh -. "$GIT_BUILD_DIR"/git-sh-i18n +GIT_INTERNAL_GETTEXT_TEST_FALLBACKS=YesPlease +export GIT_INTERNAL_GETTEXT_TEST_FALLBACKS + +. ./lib-gettext.sh + +test_expect_success "sanity: \$GIT_INTERNAL_GETTEXT_SH_SCHEME is set (to $GIT_INTERNAL_GETTEXT_SH_SCHEME)" ' + test -n "$GIT_INTERNAL_GETTEXT_SH_SCHEME" +' + +test_expect_success 'sanity: $GIT_INTERNAL_GETTEXT_TEST_FALLBACKS is set' ' + test -n "$GIT_INTERNAL_GETTEXT_TEST_FALLBACKS" +' + +test_expect_success C_LOCALE_OUTPUT 'sanity: $GIT_INTERNAL_GETTEXT_SH_SCHEME" is fallthrough' ' + echo fallthrough >expect && + echo $GIT_INTERNAL_GETTEXT_SH_SCHEME >actual && + test_cmp expect actual +' test_expect_success 'gettext: our gettext() fallback has pass-through semantics' ' printf "test" >expect && diff --git a/t/t0202-gettext-perl.sh b/t/t0202-gettext-perl.sh new file mode 100755 index 000000000..428ebb008 --- /dev/null +++ b/t/t0202-gettext-perl.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason +# + +test_description='Perl gettext interface (Git::I18N)' + +. ./lib-gettext.sh + +if ! test_have_prereq PERL; then + skip_all='skipping perl interface tests, perl not available' + test_done +fi + +"$PERL_PATH" -MTest::More -e 0 2>/dev/null || { + skip_all="Perl Test::More unavailable, skipping test" + test_done +} + +# The external test will outputs its own plan +test_external_has_tap=1 + +test_external_without_stderr \ + 'Perl Git::I18N API' \ + "$PERL_PATH" "$TEST_DIRECTORY"/t0202/test.pl + +test_done diff --git a/t/t0202/test.pl b/t/t0202/test.pl new file mode 100644 index 000000000..2c10cb469 --- /dev/null +++ b/t/t0202/test.pl @@ -0,0 +1,110 @@ +#!/usr/bin/perl +use 5.008; +use lib (split(/:/, $ENV{GITPERLLIB})); +use strict; +use warnings; +use POSIX qw(:locale_h); +use Test::More tests => 8; +use Git::I18N; + +my $has_gettext_library = $Git::I18N::__HAS_LIBRARY; + +ok(1, "Testing Git::I18N with " . + ($has_gettext_library + ? (defined $Locale::Messages::VERSION + ? "Locale::Messages version $Locale::Messages::VERSION" + # Versions of Locale::Messages before 1.17 didn't have a + # $VERSION variable. + : "Locale::Messages version <1.17") + : "NO Perl gettext library")); +ok(1, "Git::I18N is located at $INC{'Git/I18N.pm'}"); + +{ + my $exports = @Git::I18N::EXPORT; + ok($exports, "sanity: Git::I18N has $exports export(s)"); +} +is_deeply(\@Git::I18N::EXPORT, \@Git::I18N::EXPORT_OK, "sanity: Git::I18N exports everything by default"); + +# prototypes +{ + # Add prototypes here when modifying the public interface to add + # more gettext wrapper functions. + my %prototypes = (qw( + __ $ + )); + while (my ($sub, $proto) = each %prototypes) { + is(prototype(\&{"Git::I18N::$sub"}), $proto, "sanity: $sub has a $proto prototype"); + } +} + +# Test basic passthrough in the C locale +{ + local $ENV{LANGUAGE} = 'C'; + local $ENV{LC_ALL} = 'C'; + local $ENV{LANG} = 'C'; + + my ($got, $expect) = (('TEST: A Perl test string') x 2); + + is(__($got), $expect, "Passing a string through __() in the C locale works"); +} + +# Test a basic message on different locales +SKIP: { + unless ($ENV{GETTEXT_LOCALE}) { + # Can't reliably test __() with a non-C locales because the + # required locales may not be installed on the system. + # + # We test for these anyway as part of the shell + # tests. Skipping these here will eliminate failures on odd + # platforms with incomplete locale data. + + skip "GETTEXT_LOCALE must be set by lib-gettext.sh for exhaustive Git::I18N tests", 2; + } + + # The is_IS UTF-8 locale passed from lib-gettext.sh + my $is_IS_locale = $ENV{is_IS_locale}; + + my $test = sub { + my ($got, $expect, $msg, $locale) = @_; + # Maybe this system doesn't have the locale we're trying to + # test. + my $locale_ok = setlocale(LC_ALL, $locale); + is(__($got), $expect, "$msg a gettext library + <$locale> locale <$got> turns into <$expect>"); + }; + + my $env_C = sub { + $ENV{LANGUAGE} = 'C'; + $ENV{LC_ALL} = 'C'; + }; + + my $env_is = sub { + $ENV{LANGUAGE} = 'is'; + $ENV{LC_ALL} = $is_IS_locale; + }; + + # Translation's the same as the original + my ($got, $expect) = (('TEST: A Perl test string') x 2); + + if ($has_gettext_library) { + { + local %ENV; $env_C->(); + $test->($got, $expect, "With", 'C'); + } + + { + my ($got, $expect) = ($got, 'TILRAUN: Perl tilraunastrengur'); + local %ENV; $env_is->(); + $test->($got, $expect, "With", $is_IS_locale); + } + } else { + { + local %ENV; $env_C->(); + $test->($got, $expect, "Without", 'C'); + } + + { + local %ENV; $env_is->(); + $test->($got, $expect, "Without", 'is'); + } + } +} diff --git a/t/t0203-gettext-setlocale-sanity.sh b/t/t0203-gettext-setlocale-sanity.sh new file mode 100755 index 000000000..a21246008 --- /dev/null +++ b/t/t0203-gettext-setlocale-sanity.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason +# + +test_description="The Git C functions aren't broken by setlocale(3)" + +. ./lib-gettext.sh + +test_expect_success 'git show a ISO-8859-1 commit under C locale' ' + . "$TEST_DIRECTORY"/t3901-8859-1.txt && + test_commit "iso-c-commit" iso-under-c && + git show >out 2>err && + ! test -s err && + grep -q "iso-c-commit" out +' + +test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' ' + . "$TEST_DIRECTORY"/t3901-8859-1.txt && + test_commit "iso-utf8-commit" iso-under-utf8 && + LANGUAGE=is LC_ALL="$is_IS_locale" git show >out 2>err && + ! test -s err && + grep -q "iso-utf8-commit" out +' + +test_done diff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh new file mode 100755 index 000000000..189af90c0 --- /dev/null +++ b/t/t0204-gettext-reencode-sanity.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason +# + +test_description="Gettext reencoding of our *.po/*.mo files works" + +. ./lib-gettext.sh + + +test_expect_success GETTEXT_LOCALE 'gettext: Emitting UTF-8 from our UTF-8 *.mo files / Icelandic' ' + printf "TILRAUN: Halló Heimur!" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: Hello World!" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_LOCALE 'gettext: Emitting UTF-8 from our UTF-8 *.mo files / Runes' ' + printf "TILRAUN: ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: Old English Runes" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_ISO_LOCALE 'gettext: Emitting ISO-8859-1 from our UTF-8 *.mo files / Icelandic' ' + printf "TILRAUN: Halló Heimur!" | iconv -f UTF-8 -t ISO8859-1 >expect && + LANGUAGE=is LC_ALL="$is_IS_iso_locale" gettext "TEST: Hello World!" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_ISO_LOCALE 'gettext: Emitting ISO-8859-1 from our UTF-8 *.mo files / Runes' ' + LANGUAGE=is LC_ALL="$is_IS_iso_locale" gettext "TEST: Old English Runes" >runes && + + if grep "^TEST: Old English Runes$" runes + then + say "Your system can not handle this complexity and returns the string as-is" + else + # Both Solaris and GNU libintl will return this stream of + # question marks, so it is s probably portable enough + printf "TILRAUN: ?? ???? ??? ?? ???? ?? ??? ????? ??????????? ??? ?? ????" >runes-expect && + test_cmp runes-expect runes + fi +' + +test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' ' + printf "TILRAUN: ‚einfaldar‘ og „tvöfaldar“ gæsalappir" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" gettext "TEST: ‘single’ and “double” quotes" >actual && + test_cmp expect actual +' + +# How these quotes get transliterated depends on the gettext implementation: +# +# Debian: ,einfaldar' og ,,tvöfaldar" [GNU libintl] +# FreeBSD: `einfaldar` og "tvöfaldar" [GNU libintl] +# Solaris: ?einfaldar? og ?tvöfaldar? [Solaris libintl] +# +# Just make sure the contents are transliterated, and don't use grep -q +# so that these differences are emitted under --verbose for curious +# eyes. +test_expect_success GETTEXT_ISO_LOCALE 'gettext: Fetching a UTF-8 msgid -> ISO-8859-1' ' + LANGUAGE=is LC_ALL="$is_IS_iso_locale" gettext "TEST: ‘single’ and “double” quotes" >actual && + grep "einfaldar" actual && + grep "$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)" actual +' + +test_expect_success GETTEXT_LOCALE 'gettext.c: git init UTF-8 -> UTF-8' ' + printf "Bjó til tóma Git lind" >expect && + LANGUAGE=is LC_ALL="$is_IS_locale" git init repo >actual && + test_when_finished "rm -rf repo" && + grep "^$(cat expect) " actual +' + +test_expect_success GETTEXT_ISO_LOCALE 'gettext.c: git init UTF-8 -> ISO-8859-1' ' + printf "Bjó til tóma Git lind" >expect && + LANGUAGE=is LC_ALL="$is_IS_iso_locale" git init repo >actual && + test_when_finished "rm -rf repo" && + grep "^$(cat expect | iconv -f UTF-8 -t ISO8859-1) " actual +' + +test_done diff --git a/t/t0205-gettext-poison.sh b/t/t0205-gettext-poison.sh new file mode 100755 index 000000000..2361590d5 --- /dev/null +++ b/t/t0205-gettext-poison.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# +# Copyright (c) 2010 Ævar Arnfjörð Bjarmason +# + +test_description='Gettext Shell poison' + +. ./lib-gettext.sh + +test_expect_success GETTEXT_POISON "sanity: \$GIT_INTERNAL_GETTEXT_SH_SCHEME is set (to $GIT_INTERNAL_GETTEXT_SH_SCHEME)" ' + test -n "$GIT_INTERNAL_GETTEXT_SH_SCHEME" +' + +test_expect_success GETTEXT_POISON 'sanity: $GIT_INTERNAL_GETTEXT_SH_SCHEME" is poison' ' + test "$GIT_INTERNAL_GETTEXT_SH_SCHEME" = "poison" +' + +test_expect_success GETTEXT_POISON 'gettext: our gettext() fallback has poison semantics' ' + printf "# GETTEXT POISON #" >expect && + gettext "test" >actual && + test_cmp expect actual && + printf "# GETTEXT POISON #" >expect && + gettext "test more words" >actual && + test_cmp expect actual +' + +test_expect_success GETTEXT_POISON 'eval_gettext: our eval_gettext() fallback has poison semantics' ' + printf "# GETTEXT POISON #" >expect && + eval_gettext "test" >actual && + test_cmp expect actual && + printf "# GETTEXT POISON #" >expect && + eval_gettext "test more words" >actual && + test_cmp expect actual +' + +test_done diff --git a/t/test-lib.sh b/t/test-lib.sh index 160479b81..0e932dd97 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -44,6 +44,7 @@ export LANG LC_ALL PAGER TERM TZ EDITOR=: unset VISUAL unset EMAIL +unset LANGUAGE unset $(perl -e ' my @env = keys %ENV; my $ok = join("|", qw( @@ -1118,12 +1119,14 @@ esac test -z "$NO_PERL" && test_set_prereq PERL test -z "$NO_PYTHON" && test_set_prereq PYTHON test -n "$USE_LIBPCRE" && test_set_prereq LIBPCRE +test -z "$NO_GETTEXT" && test_set_prereq GETTEXT # Can we rely on git's output in the C locale? if test -n "$GETTEXT_POISON" then GIT_GETTEXT_POISON=YesPlease export GIT_GETTEXT_POISON + test_set_prereq GETTEXT_POISON else test_set_prereq C_LOCALE_OUTPUT fi diff --git a/upload-pack.c b/upload-pack.c index 470cffd7c..6f36f6255 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -784,6 +784,8 @@ int main(int argc, char **argv) int i; int strict = 0; + git_setup_gettext(); + packet_trace_identity("upload-pack"); git_extract_argv0_path(argv[0]); read_replace_refs = 0; diff --git a/wrap-for-bin.sh b/wrap-for-bin.sh index 09feb1f73..53a8dd0a3 100644 --- a/wrap-for-bin.sh +++ b/wrap-for-bin.sh @@ -15,7 +15,8 @@ else export GIT_TEMPLATE_DIR fi GITPERLLIB='@@BUILD_DIR@@/perl/blib/lib' +GIT_TEXTDOMAINDIR='@@BUILD_DIR@@/po/build/locale' PATH='@@BUILD_DIR@@/bin-wrappers:'"$PATH" -export GIT_EXEC_PATH GITPERLLIB PATH +export GIT_EXEC_PATH GITPERLLIB PATH GIT_TEXTDOMAINDIR exec "${GIT_EXEC_PATH}/@@PROG@@" "$@"