mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +00:00 
			
		
		
		
	Add a per-message fallback mechanism for translations.
This commit is contained in:
		
							parent
							
								
									1be6419871
								
							
						
					
					
						commit
						a55ffaeee9
					
				
					 3 changed files with 84 additions and 26 deletions
				
			
		| 
						 | 
					@ -95,7 +95,8 @@ for returning either standard 8-bit strings or Unicode strings.
 | 
				
			||||||
Translations instances can also install themselves in the built-in
 | 
					Translations instances can also install themselves in the built-in
 | 
				
			||||||
namespace as the function \function{_()}.
 | 
					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
 | 
					This function implements the standard \file{.mo} file search
 | 
				
			||||||
algorithm.  It takes a \var{domain}, identical to what
 | 
					algorithm.  It takes a \var{domain}, identical to what
 | 
				
			||||||
\function{textdomain()} takes.  Optional \var{localedir} is as in
 | 
					\function{textdomain()} takes.  Optional \var{localedir} is as in
 | 
				
			||||||
| 
						 | 
					@ -119,7 +120,9 @@ components:
 | 
				
			||||||
\file{\var{localedir}/\var{language}/LC_MESSAGES/\var{domain}.mo}
 | 
					\file{\var{localedir}/\var{language}/LC_MESSAGES/\var{domain}.mo}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The first such file name that exists is returned by \function{find()}.
 | 
					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}
 | 
					\end{funcdesc}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\begin{funcdesc}{translation}{domain\optional{, localedir\optional{,
 | 
					\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}}}}}
 | 
					                              class_,\optional{fallback}}}}}
 | 
				
			||||||
Return a \class{Translations} instance based on the \var{domain},
 | 
					Return a \class{Translations} instance based on the \var{domain},
 | 
				
			||||||
\var{localedir}, and \var{languages}, which are first passed to
 | 
					\var{localedir}, and \var{languages}, which are first passed to
 | 
				
			||||||
\function{find()} to get the
 | 
					\function{find()} to get a list of the
 | 
				
			||||||
associated \file{.mo} file path.  Instances with
 | 
					associated \file{.mo} file paths.  Instances with
 | 
				
			||||||
identical \file{.mo} file names are cached.  The actual class instantiated
 | 
					identical \file{.mo} file names are cached.  The actual class instantiated
 | 
				
			||||||
is either \var{class_} if provided, otherwise
 | 
					is either \var{class_} if provided, otherwise
 | 
				
			||||||
\class{GNUTranslations}.  The class's constructor must take a single
 | 
					\class{GNUTranslations}.  The class's constructor must take a single
 | 
				
			||||||
file object argument.  If no \file{.mo} file is found, this
 | 
					file object argument.  
 | 
				
			||||||
function raises \exception{IOError} if \var{fallback} is false
 | 
					
 | 
				
			||||||
(which is the default), and returns a \class{NullTranslations} instance
 | 
					If multiple files are found, later files are used as fallbacks for
 | 
				
			||||||
if \var{fallback} is true.
 | 
					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}
 | 
					\end{funcdesc}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\begin{funcdesc}{install}{domain\optional{, localedir\optional{, unicode}}}
 | 
					\begin{funcdesc}{install}{domain\optional{, localedir\optional{, unicode}}}
 | 
				
			||||||
| 
						 | 
					@ -168,7 +178,8 @@ methods of \class{NullTranslations}:
 | 
				
			||||||
\begin{methoddesc}[NullTranslations]{__init__}{\optional{fp}}
 | 
					\begin{methoddesc}[NullTranslations]{__init__}{\optional{fp}}
 | 
				
			||||||
Takes an optional file object \var{fp}, which is ignored by the base
 | 
					Takes an optional file object \var{fp}, which is ignored by the base
 | 
				
			||||||
class.  Initializes ``protected'' instance variables \var{_info} and
 | 
					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}.
 | 
					\code{self._parse(fp)} if \var{fp} is not \code{None}.
 | 
				
			||||||
\end{methoddesc}
 | 
					\end{methoddesc}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -179,13 +190,21 @@ you have an unsupported message catalog file format, you should
 | 
				
			||||||
override this method to parse your format.
 | 
					override this method to parse your format.
 | 
				
			||||||
\end{methoddesc}
 | 
					\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}
 | 
					\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}
 | 
					\end{methoddesc}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\begin{methoddesc}[NullTranslations]{ugettext}{message}
 | 
					\begin{methoddesc}[NullTranslations]{ugettext}{message}
 | 
				
			||||||
Return the translated message as a Unicode string.  Overridden in
 | 
					If a fallback has been set, forward \method{ugettext} to the fallback.
 | 
				
			||||||
derived classes.
 | 
					Otherwise, return the translated message as a Unicode string.
 | 
				
			||||||
 | 
					Overridden in derived classes.
 | 
				
			||||||
\end{methoddesc}
 | 
					\end{methoddesc}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\begin{methoddesc}[NullTranslations]{info}{}
 | 
					\begin{methoddesc}[NullTranslations]{info}{}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import struct
 | 
					import struct
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
from errno import ENOENT
 | 
					from errno import ENOENT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ["bindtextdomain","textdomain","gettext","dgettext",
 | 
					__all__ = ["bindtextdomain","textdomain","gettext","dgettext",
 | 
				
			||||||
| 
						 | 
					@ -102,16 +103,27 @@ class NullTranslations:
 | 
				
			||||||
    def __init__(self, fp=None):
 | 
					    def __init__(self, fp=None):
 | 
				
			||||||
        self._info = {}
 | 
					        self._info = {}
 | 
				
			||||||
        self._charset = None
 | 
					        self._charset = None
 | 
				
			||||||
 | 
					        self._fallback = None
 | 
				
			||||||
        if fp:
 | 
					        if fp:
 | 
				
			||||||
            self._parse(fp)
 | 
					            self._parse(fp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _parse(self, fp):
 | 
					    def _parse(self, fp):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_fallback(self, fallback):
 | 
				
			||||||
 | 
					        if self._fallback:
 | 
				
			||||||
 | 
					            self._fallback.add_fallback(fallback)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self._fallback = fallback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gettext(self, message):
 | 
					    def gettext(self, message):
 | 
				
			||||||
 | 
					        if self._fallback:
 | 
				
			||||||
 | 
					            return self._fallback.gettext(message)
 | 
				
			||||||
        return message
 | 
					        return message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ugettext(self, message):
 | 
					    def ugettext(self, message):
 | 
				
			||||||
 | 
					        if self._fallback:
 | 
				
			||||||
 | 
					            return self._fallback.ugettext(message)
 | 
				
			||||||
        return unicode(message)
 | 
					        return unicode(message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def info(self):
 | 
					    def info(self):
 | 
				
			||||||
| 
						 | 
					@ -188,16 +200,26 @@ def _parse(self, fp):
 | 
				
			||||||
            transidx += 8
 | 
					            transidx += 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gettext(self, message):
 | 
					    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):
 | 
					    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)
 | 
					        return unicode(tmsg, self._charset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Locate a .mo file using the gettext strategy
 | 
					# 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
 | 
					    # Get some reasonable defaults for arguments that were not supplied
 | 
				
			||||||
    if localedir is None:
 | 
					    if localedir is None:
 | 
				
			||||||
        localedir = _default_localedir
 | 
					        localedir = _default_localedir
 | 
				
			||||||
| 
						 | 
					@ -217,13 +239,20 @@ def find(domain, localedir=None, languages=None):
 | 
				
			||||||
            if nelang not in nelangs:
 | 
					            if nelang not in nelangs:
 | 
				
			||||||
                nelangs.append(nelang)
 | 
					                nelangs.append(nelang)
 | 
				
			||||||
    # select a language
 | 
					    # select a language
 | 
				
			||||||
 | 
					    if all:
 | 
				
			||||||
 | 
					        result = []
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        result = None
 | 
				
			||||||
    for lang in nelangs:
 | 
					    for lang in nelangs:
 | 
				
			||||||
        if lang == 'C':
 | 
					        if lang == 'C':
 | 
				
			||||||
            break
 | 
					            break
 | 
				
			||||||
        mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)
 | 
					        mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)
 | 
				
			||||||
        if os.path.exists(mofile):
 | 
					        if os.path.exists(mofile):
 | 
				
			||||||
            return mofile
 | 
					            if all:
 | 
				
			||||||
    return None
 | 
					                result.append(mofile)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return mofile
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -234,20 +263,28 @@ def translation(domain, localedir=None, languages=None,
 | 
				
			||||||
                class_=None, fallback=0):
 | 
					                class_=None, fallback=0):
 | 
				
			||||||
    if class_ is None:
 | 
					    if class_ is None:
 | 
				
			||||||
        class_ = GNUTranslations
 | 
					        class_ = GNUTranslations
 | 
				
			||||||
    mofile = find(domain, localedir, languages)
 | 
					    mofiles = find(domain, localedir, languages, all=1)
 | 
				
			||||||
    if mofile is None:
 | 
					    if len(mofiles)==0:
 | 
				
			||||||
        if fallback:
 | 
					        if fallback:
 | 
				
			||||||
            return NullTranslations()
 | 
					            return NullTranslations()
 | 
				
			||||||
        raise IOError(ENOENT, 'No translation file found for domain', domain)
 | 
					        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?
 | 
					    # 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
 | 
					    # Avoid opening, reading, and parsing the .mo file after it's been done
 | 
				
			||||||
    # once.
 | 
					    # once.
 | 
				
			||||||
    t = _translations.get(key)
 | 
					    result = None
 | 
				
			||||||
    if t is None:
 | 
					    for mofile in mofiles:
 | 
				
			||||||
        t = _translations.setdefault(key, class_(open(mofile, 'rb')))
 | 
					        key = os.path.abspath(mofile)
 | 
				
			||||||
    return t
 | 
					        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):
 | 
					def install(domain, localedir=None, unicode=0):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,9 @@ Library
 | 
				
			||||||
  arbitrary shell code can't be executed because a bogus URL was
 | 
					  arbitrary shell code can't be executed because a bogus URL was
 | 
				
			||||||
  passed in.
 | 
					  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
 | 
					Tools/Demos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue