Commit a55ffaee authored by Martin v. Löwis's avatar Martin v. Löwis

Add a per-message fallback mechanism for translations.

parent 1be64198
...@@ -95,7 +95,8 @@ for returning either standard 8-bit strings or Unicode strings. ...@@ -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: ...@@ -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. ...@@ -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}: ...@@ -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 ...@@ -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 @@ internationalized, to the local language and cultural habits. ...@@ -46,6 +46,7 @@ internationalized, to the local language and cultural habits.
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: ...@@ -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 @@ class GNUTranslations(NullTranslations): ...@@ -188,16 +200,26 @@ class GNUTranslations(NullTranslations):
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): ...@@ -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, ...@@ -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 ...@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment