From a55ffaeee9eb2fba62f280c364b3460fafd9efc9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20v=2E=20L=C3=B6wis?= <martin@v.loewis.de>
Date: Fri, 11 Jan 2002 06:58:49 +0000
Subject: Add a per-message fallback mechanism for translations.

---
 Doc/lib/libgettext.tex | 43 ++++++++++++++++++++++++----------
 Lib/gettext.py         | 63 +++++++++++++++++++++++++++++++++++++++-----------
 Misc/NEWS              |  4 +++-
 3 files changed, 84 insertions(+), 26 deletions(-)

diff --git a/Doc/lib/libgettext.tex b/Doc/lib/libgettext.tex
index be22340..c19d9b9 100644
--- a/Doc/lib/libgettext.tex
+++ b/Doc/lib/libgettext.tex
@@ -95,7 +95,8 @@ for returning either standard 8-bit strings or Unicode strings.
 Translations instances can also install themselves in the built-in
 namespace as the function \function{_()}.
 
-\begin{funcdesc}{find}{domain\optional{, localedir\optional{, languages}}}
+\begin{funcdesc}{find}{domain\optional{, localedir\optional{, 
+                        languages\optional{, all}}}}
 This function implements the standard \file{.mo} file search
 algorithm.  It takes a \var{domain}, identical to what
 \function{textdomain()} takes.  Optional \var{localedir} is as in
@@ -119,7 +120,9 @@ components:
 \file{\var{localedir}/\var{language}/LC_MESSAGES/\var{domain}.mo}
 
 The first such file name that exists is returned by \function{find()}.
-If no such file is found, then \code{None} is returned.
+If no such file is found, then \code{None} is returned. If \var{all}
+is given, it returns a list of all file names, in the order in which
+they appear in the languages list or the environment variables.
 \end{funcdesc}
 
 \begin{funcdesc}{translation}{domain\optional{, localedir\optional{,
@@ -127,15 +130,22 @@ If no such file is found, then \code{None} is returned.
                               class_,\optional{fallback}}}}}
 Return a \class{Translations} instance based on the \var{domain},
 \var{localedir}, and \var{languages}, which are first passed to
-\function{find()} to get the
-associated \file{.mo} file path.  Instances with
+\function{find()} to get a list of the
+associated \file{.mo} file paths.  Instances with
 identical \file{.mo} file names are cached.  The actual class instantiated
 is either \var{class_} if provided, otherwise
 \class{GNUTranslations}.  The class's constructor must take a single
-file object argument.  If no \file{.mo} file is found, this
-function raises \exception{IOError} if \var{fallback} is false
-(which is the default), and returns a \class{NullTranslations} instance
-if \var{fallback} is true.
+file object argument.  
+
+If multiple files are found, later files are used as fallbacks for
+earlier ones. To allow setting the fallback, \function{copy.copy}
+is used to clone each translation object from the cache; the actual
+instance data is still shared with the cache.
+
+If no \file{.mo} file is found, this function raises
+\exception{IOError} if \var{fallback} is false (which is the default),
+and returns a \class{NullTranslations} instance if \var{fallback} is
+true.
 \end{funcdesc}
 
 \begin{funcdesc}{install}{domain\optional{, localedir\optional{, unicode}}}
@@ -168,7 +178,8 @@ methods of \class{NullTranslations}:
 \begin{methoddesc}[NullTranslations]{__init__}{\optional{fp}}
 Takes an optional file object \var{fp}, which is ignored by the base
 class.  Initializes ``protected'' instance variables \var{_info} and
-\var{_charset} which are set by derived classes.  It then calls
+\var{_charset} which are set by derived classes, as well as \var{_fallback},
+which is set through \method{add_fallback}.  It then calls
 \code{self._parse(fp)} if \var{fp} is not \code{None}.
 \end{methoddesc}
 
@@ -179,13 +190,21 @@ you have an unsupported message catalog file format, you should
 override this method to parse your format.
 \end{methoddesc}
 
+\begin{methoddesc}{NullTranslations}{add_fallback}{fallback}
+Add \var{fallback} as the fallback object for the current translation
+object. A translation object should consult the fallback if it cannot
+provide a translation for a given message.
+\end{methoddesc}
+
 \begin{methoddesc}[NullTranslations]{gettext}{message}
-Return the translated message.  Overridden in derived classes.
+If a fallback has been set, forward \method{gettext} to the fallback.
+Otherwise, return the translated message.  Overridden in derived classes.
 \end{methoddesc}
 
 \begin{methoddesc}[NullTranslations]{ugettext}{message}
-Return the translated message as a Unicode string.  Overridden in
-derived classes.
+If a fallback has been set, forward \method{ugettext} to the fallback.
+Otherwise, return the translated message as a Unicode string.
+Overridden in derived classes.
 \end{methoddesc}
 
 \begin{methoddesc}[NullTranslations]{info}{}
diff --git a/Lib/gettext.py b/Lib/gettext.py
index 6795ee6..0bea9ed 100644
--- a/Lib/gettext.py
+++ b/Lib/gettext.py
@@ -46,6 +46,7 @@ internationalized, to the local language and cultural habits.
 import os
 import sys
 import struct
+import copy
 from errno import ENOENT
 
 __all__ = ["bindtextdomain","textdomain","gettext","dgettext",
@@ -102,16 +103,27 @@ class NullTranslations:
     def __init__(self, fp=None):
         self._info = {}
         self._charset = None
+        self._fallback = None
         if fp:
             self._parse(fp)
 
     def _parse(self, fp):
         pass
 
+    def add_fallback(self, fallback):
+        if self._fallback:
+            self._fallback.add_fallback(fallback)
+        else:
+            self._fallback = fallback
+
     def gettext(self, message):
+        if self._fallback:
+            return self._fallback.gettext(message)
         return message
 
     def ugettext(self, message):
+        if self._fallback:
+            return self._fallback.ugettext(message)
         return unicode(message)
 
     def info(self):
@@ -188,16 +200,26 @@ class GNUTranslations(NullTranslations):
             transidx += 8
 
     def gettext(self, message):
-        return self._catalog.get(message, message)
+        try:
+            return self._catalog[message]
+        except KeyError:
+            if self._fallback:
+                return self._fallback.gettext(message)
+            return message
 
     def ugettext(self, message):
-        tmsg = self._catalog.get(message, message)
+        try:
+            tmsg = self._catalog[message]
+        except KeyError:
+            if self._fallback:
+                return self._fallback.ugettext(message)
+            tmsg = message
         return unicode(tmsg, self._charset)
 
 
 
 # Locate a .mo file using the gettext strategy
-def find(domain, localedir=None, languages=None):
+def find(domain, localedir=None, languages=None, all=0):
     # Get some reasonable defaults for arguments that were not supplied
     if localedir is None:
         localedir = _default_localedir
@@ -217,13 +239,20 @@ def find(domain, localedir=None, languages=None):
             if nelang not in nelangs:
                 nelangs.append(nelang)
     # select a language
+    if all:
+        result = []
+    else:
+        result = None
     for lang in nelangs:
         if lang == 'C':
             break
         mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)
         if os.path.exists(mofile):
-            return mofile
-    return None
+            if all:
+                result.append(mofile)
+            else:
+                return mofile
+    return result
 
 
 
@@ -234,20 +263,28 @@ def translation(domain, localedir=None, languages=None,
                 class_=None, fallback=0):
     if class_ is None:
         class_ = GNUTranslations
-    mofile = find(domain, localedir, languages)
-    if mofile is None:
+    mofiles = find(domain, localedir, languages, all=1)
+    if len(mofiles)==0:
         if fallback:
             return NullTranslations()
         raise IOError(ENOENT, 'No translation file found for domain', domain)
-    key = os.path.abspath(mofile)
     # TBD: do we need to worry about the file pointer getting collected?
     # Avoid opening, reading, and parsing the .mo file after it's been done
     # once.
-    t = _translations.get(key)
-    if t is None:
-        t = _translations.setdefault(key, class_(open(mofile, 'rb')))
-    return t
-
+    result = None
+    for mofile in mofiles:
+        key = os.path.abspath(mofile)
+        t = _translations.get(key)
+        if t is None:
+            t = _translations.setdefault(key, class_(open(mofile, 'rb')))
+        # Copy the translation object to allow setting fallbacks.
+        # All other instance data is shared with the cached object.
+        t = copy.copy(t)
+        if result is None:
+            result = t
+        else:
+            result.add_fallback(t)
+    return result
 
 
 def install(domain, localedir=None, unicode=0):
diff --git a/Misc/NEWS b/Misc/NEWS
index cc81150..355c926 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -26,7 +26,9 @@ Library
   arbitrary shell code can't be executed because a bogus URL was
   passed in.
 
-- gettext.translation has an optional fallback argument.
+- gettext.translation has an optional fallback argument, and 
+  gettext.find an optional all argument. Translations will now fallback
+  on a per-message basis.
 
 Tools/Demos
 
-- 
cgit v0.12

