1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: i18n.py,v 1.15 2005-06-14 05:33:32 a1s Exp $
20 """
21 RoundUp Internationalization (I18N)
23 To use this module, the following code should be used::
25 from roundup.i18n import _
26 ...
27 print _("Some text that can be translated")
29 Note that to enable re-ordering of inserted texts in formatting strings
30 (which can easily happen if a sentence has to be re-ordered due to
31 grammatical changes), translatable formats should use named format specs::
33 ... _('Index of %(classname)s') % {'classname': cn} ...
35 Also, this eases the job of translators since they have some context what
36 the dynamic portion of a message really means.
37 """
38 __docformat__ = 'restructuredtext'
40 import errno
41 import gettext as gettext_module
42 import os
44 from roundup import msgfmt
46 # List of directories for mo file search (see SF bug 1219689)
47 LOCALE_DIRS = [
48 gettext_module._default_localedir,
49 ]
50 # compute mo location relative to roundup installation directory
51 # (prefix/lib/python/site-packages/roundup/msgfmt.py on posix systems,
52 # prefix/lib/site-packages/roundup/msgfmt.py on windows).
53 # locale root is prefix/share/locale.
54 if os.name == "nt":
55 _mo_path = [".."] * 4 + ["share", "locale"]
56 else:
57 _mo_path = [".."] * 5 + ["share", "locale"]
58 _mo_path = os.path.normpath(os.path.join(msgfmt.__file__, *_mo_path))
59 if _mo_path not in LOCALE_DIRS:
60 LOCALE_DIRS.append(_mo_path)
61 del _mo_path
63 # Roundup text domain
64 DOMAIN = "roundup"
66 if hasattr(gettext_module.GNUTranslations, "ngettext"):
67 # gettext_module has everything needed
68 RoundupNullTranslations = gettext_module.NullTranslations
69 RoundupTranslations = gettext_module.GNUTranslations
70 else:
71 # prior to 2.3, there was no plural forms. mix simple emulation in
72 class PluralFormsMixIn:
73 def ngettext(self, singular, plural, count):
74 if count == 1:
75 _msg = singular
76 else:
77 _msg = plural
78 return self.gettext(_msg)
79 def ungettext(self, singular, plural, count):
80 if count == 1:
81 _msg = singular
82 else:
83 _msg = plural
84 return self.ugettext(_msg)
85 class RoundupNullTranslations(
86 gettext_module.NullTranslations, PluralFormsMixIn
87 ):
88 pass
89 class RoundupTranslations(
90 gettext_module.GNUTranslations, PluralFormsMixIn
91 ):
92 pass
94 def find_locales(language=None):
95 """Return normalized list of locale names to try for given language
97 Argument 'language' may be a single language code or a list of codes.
98 If 'language' is omitted or None, use locale settings in OS environment.
100 """
101 # body of this function is borrowed from gettext_module.find()
102 if language is None:
103 languages = []
104 for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
105 val = os.environ.get(envar)
106 if val:
107 languages = val.split(':')
108 break
109 elif isinstance(language, str) or isinstance(language, unicode):
110 languages = [language]
111 else:
112 # 'language' must be iterable
113 languages = language
114 # now normalize and expand the languages
115 nelangs = []
116 for lang in languages:
117 for nelang in gettext_module._expand_lang(lang):
118 if nelang not in nelangs:
119 nelangs.append(nelang)
120 return nelangs
122 def get_mofile(languages, localedir, domain=None):
123 """Return the first of .mo files found in localedir for languages
125 Parameters:
126 languages:
127 list of locale names to try
128 localedir:
129 path to directory containing locale files.
130 Usually this is either gettext_module._default_localedir
131 or 'locale' subdirectory in the tracker home.
132 domain:
133 optional name of messages domain.
134 If omitted or None, work with simplified
135 locale directory, as used in tracker homes:
136 message catalogs are kept in files locale.po
137 instead of locale/LC_MESSAGES/domain.po
139 Return the path of the first .mo file found.
140 If nothing found, return None.
142 Automatically compile .po files if necessary.
144 """
145 for locale in languages:
146 if locale == "C":
147 break
148 if domain:
149 basename = os.path.join(localedir, locale, "LC_MESSAGES", domain)
150 else:
151 basename = os.path.join(localedir, locale)
152 # look for message catalog files, check timestamps
153 mofile = basename + ".mo"
154 if os.path.isfile(mofile):
155 motime = os.path.getmtime(mofile)
156 else:
157 motime = 0
158 pofile = basename + ".po"
159 if os.path.isfile(pofile):
160 potime = os.path.getmtime(pofile)
161 else:
162 potime = 0
163 # see what we've found
164 if motime < potime:
165 # compile
166 msgfmt.make(pofile, mofile)
167 elif motime == 0:
168 # no files found - proceed to the next locale name
169 continue
170 # .mo file found or made
171 return mofile
172 return None
174 def get_translation(language=None, tracker_home=None,
175 translation_class=RoundupTranslations,
176 null_translation_class=RoundupNullTranslations
177 ):
178 """Return Translation object for given language and domain
180 Argument 'language' may be a single language code or a list of codes.
181 If 'language' is omitted or None, use locale settings in OS environment.
183 Arguments 'translation_class' and 'null_translation_class'
184 specify the classes that are instantiated for existing
185 and non-existing translations, respectively.
187 """
188 mofiles = []
189 # locale directory paths
190 if tracker_home is None:
191 tracker_locale = None
192 else:
193 tracker_locale = os.path.join(tracker_home, "locale")
194 # get the list of locales
195 locales = find_locales(language)
196 # add mofiles found in the tracker, then in the system locale directory
197 if tracker_locale:
198 mofiles.append(get_mofile(locales, tracker_locale))
199 for system_locale in LOCALE_DIRS:
200 mofiles.append(get_mofile(locales, system_locale, DOMAIN))
201 # we want to fall back to english unless english is selected language
202 if "en" not in locales:
203 locales = find_locales("en")
204 # add mofiles found in the tracker, then in the system locale directory
205 if tracker_locale:
206 mofiles.append(get_mofile(locales, tracker_locale))
207 for system_locale in LOCALE_DIRS:
208 mofiles.append(get_mofile(locales, system_locale, DOMAIN))
209 # filter out elements that are not found
210 mofiles = filter(None, mofiles)
211 if mofiles:
212 translator = translation_class(open(mofiles[0], "rb"))
213 for mofile in mofiles[1:]:
214 # note: current implementation of gettext_module
215 # always adds fallback to the end of the fallback chain.
216 translator.add_fallback(translation_class(open(mofile, "rb")))
217 else:
218 translator = null_translation_class()
219 return translator
221 # static translations object
222 translation = get_translation()
223 # static translation functions
224 _ = gettext = translation.gettext
225 ugettext = translation.ugettext
226 ngettext = translation.ngettext
227 ungettext = translation.ungettext
229 # vim: set filetype=python sts=4 sw=4 et si :