~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to vendor/tornado/tornado/locale.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
#
 
3
# Copyright 2009 Facebook
 
4
#
 
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
 
6
# not use this file except in compliance with the License. You may obtain
 
7
# a copy of the License at
 
8
#
 
9
#     http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
# Unless required by applicable law or agreed to in writing, software
 
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
14
# License for the specific language governing permissions and limitations
 
15
# under the License.
 
16
 
 
17
"""Translation methods for generating localized strings.
 
18
 
 
19
To load a locale and generate a translated string:
 
20
 
 
21
    user_locale = locale.get("es_LA")
 
22
    print user_locale.translate("Sign out")
 
23
 
 
24
locale.get() returns the closest matching locale, not necessarily the
 
25
specific locale you requested. You can support pluralization with
 
26
additional arguments to translate(), e.g.:
 
27
 
 
28
    people = [...]
 
29
    message = user_locale.translate(
 
30
        "%(list)s is online", "%(list)s are online", len(people))
 
31
    print message % {"list": user_locale.list(people)}
 
32
 
 
33
The first string is chosen if len(people) == 1, otherwise the second
 
34
string is chosen.
 
35
 
 
36
Applications should call one of load_translations (which uses a simple
 
37
CSV format) or load_gettext_translations (which uses the .mo format
 
38
supported by gettext and related tools).  If neither method is called,
 
39
the locale.translate method will simply return the original string.
 
40
"""
 
41
 
 
42
import csv
 
43
import datetime
 
44
import logging
 
45
import os
 
46
import os.path
 
47
import re
 
48
 
 
49
_default_locale = "en_US"
 
50
_translations = {}
 
51
_supported_locales = frozenset([_default_locale])
 
52
_use_gettext = False
 
53
 
 
54
_log = logging.getLogger('tornado.locale')
 
55
 
 
56
def get(*locale_codes):
 
57
    """Returns the closest match for the given locale codes.
 
58
 
 
59
    We iterate over all given locale codes in order. If we have a tight
 
60
    or a loose match for the code (e.g., "en" for "en_US"), we return
 
61
    the locale. Otherwise we move to the next code in the list.
 
62
 
 
63
    By default we return en_US if no translations are found for any of
 
64
    the specified locales. You can change the default locale with
 
65
    set_default_locale() below.
 
66
    """
 
67
    return Locale.get_closest(*locale_codes)
 
68
 
 
69
 
 
70
def set_default_locale(code):
 
71
    """Sets the default locale, used in get_closest_locale().
 
72
 
 
73
    The default locale is assumed to be the language used for all strings
 
74
    in the system. The translations loaded from disk are mappings from
 
75
    the default locale to the destination locale. Consequently, you don't
 
76
    need to create a translation file for the default locale.
 
77
    """
 
78
    global _default_locale
 
79
    global _supported_locales
 
80
    _default_locale = code
 
81
    _supported_locales = frozenset(_translations.keys() + [_default_locale])
 
82
 
 
83
 
 
84
def load_translations(directory):
 
85
    """Loads translations from CSV files in a directory.
 
86
 
 
87
    Translations are strings with optional Python-style named placeholders
 
88
    (e.g., "My name is %(name)s") and their associated translations.
 
89
 
 
90
    The directory should have translation files of the form LOCALE.csv,
 
91
    e.g. es_GT.csv. The CSV files should have two or three columns: string,
 
92
    translation, and an optional plural indicator. Plural indicators should
 
93
    be one of "plural" or "singular". A given string can have both singular
 
94
    and plural forms. For example "%(name)s liked this" may have a
 
95
    different verb conjugation depending on whether %(name)s is one
 
96
    name or a list of names. There should be two rows in the CSV file for
 
97
    that string, one with plural indicator "singular", and one "plural".
 
98
    For strings with no verbs that would change on translation, simply
 
99
    use "unknown" or the empty string (or don't include the column at all).
 
100
 
 
101
    Example translation es_LA.csv:
 
102
 
 
103
        "I love you","Te amo"
 
104
        "%(name)s liked this","A %(name)s les gust\xf3 esto","plural"
 
105
        "%(name)s liked this","A %(name)s le gust\xf3 esto","singular"
 
106
 
 
107
    """
 
108
    global _translations
 
109
    global _supported_locales
 
110
    _translations = {}
 
111
    for path in os.listdir(directory):
 
112
        if not path.endswith(".csv"): continue
 
113
        locale, extension = path.split(".")
 
114
        if locale not in LOCALE_NAMES:
 
115
            _log.error("Unrecognized locale %r (path: %s)", locale,
 
116
                          os.path.join(directory, path))
 
117
            continue
 
118
        f = open(os.path.join(directory, path), "r")
 
119
        _translations[locale] = {}
 
120
        for i, row in enumerate(csv.reader(f)):
 
121
            if not row or len(row) < 2: continue
 
122
            row = [c.decode("utf-8").strip() for c in row]
 
123
            english, translation = row[:2]
 
124
            if len(row) > 2:
 
125
                plural = row[2] or "unknown"
 
126
            else:
 
127
                plural = "unknown"
 
128
            if plural not in ("plural", "singular", "unknown"):
 
129
                _log.error("Unrecognized plural indicator %r in %s line %d",
 
130
                              plural, path, i + 1)
 
131
                continue
 
132
            _translations[locale].setdefault(plural, {})[english] = translation
 
133
        f.close()
 
134
    _supported_locales = frozenset(_translations.keys() + [_default_locale])
 
135
    _log.info("Supported locales: %s", sorted(_supported_locales))
 
136
 
 
137
def load_gettext_translations(directory, domain):
 
138
    """Loads translations from gettext's locale tree
 
139
 
 
140
    Locale tree is similar to system's /usr/share/locale, like:
 
141
 
 
142
    {directory}/{lang}/LC_MESSAGES/{domain}.mo
 
143
 
 
144
    Three steps are required to have you app translated:
 
145
 
 
146
    1. Generate POT translation file
 
147
        xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc
 
148
 
 
149
    2. Merge against existing POT file:
 
150
        msgmerge old.po cyclone.po > new.po
 
151
 
 
152
    3. Compile:
 
153
        msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo
 
154
    """
 
155
    import gettext
 
156
    global _translations
 
157
    global _supported_locales
 
158
    global _use_gettext
 
159
    _translations = {}
 
160
    for lang in os.listdir(directory):
 
161
        if os.path.isfile(os.path.join(directory, lang)): continue
 
162
        try:
 
163
            os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain+".mo"))
 
164
            _translations[lang] = gettext.translation(domain, directory,
 
165
                                                      languages=[lang])
 
166
        except Exception, e:
 
167
            logging.error("Cannot load translation for '%s': %s", lang, str(e))
 
168
            continue
 
169
    _supported_locales = frozenset(_translations.keys() + [_default_locale])
 
170
    _use_gettext = True
 
171
    _log.info("Supported locales: %s", sorted(_supported_locales))
 
172
 
 
173
 
 
174
def get_supported_locales(cls):
 
175
    """Returns a list of all the supported locale codes."""
 
176
    return _supported_locales
 
177
 
 
178
 
 
179
class Locale(object):
 
180
    @classmethod
 
181
    def get_closest(cls, *locale_codes):
 
182
        """Returns the closest match for the given locale code."""
 
183
        for code in locale_codes:
 
184
            if not code: continue
 
185
            code = code.replace("-", "_")
 
186
            parts = code.split("_")
 
187
            if len(parts) > 2:
 
188
                continue
 
189
            elif len(parts) == 2:
 
190
                code = parts[0].lower() + "_" + parts[1].upper()
 
191
            if code in _supported_locales:
 
192
                return cls.get(code)
 
193
            if parts[0].lower() in _supported_locales:
 
194
                return cls.get(parts[0].lower())
 
195
        return cls.get(_default_locale)
 
196
 
 
197
    @classmethod
 
198
    def get(cls, code):
 
199
        """Returns the Locale for the given locale code.
 
200
 
 
201
        If it is not supported, we raise an exception.
 
202
        """
 
203
        if not hasattr(cls, "_cache"):
 
204
            cls._cache = {}
 
205
        if code not in cls._cache:
 
206
            assert code in _supported_locales
 
207
            translations = _translations.get(code, None)
 
208
            if translations is None:
 
209
                locale = CSVLocale(code, {})
 
210
            elif _use_gettext:
 
211
                locale = GettextLocale(code, translations)
 
212
            else:
 
213
                locale = CSVLocale(code, translations)
 
214
            cls._cache[code] = locale
 
215
        return cls._cache[code]
 
216
 
 
217
    def __init__(self, code, translations):
 
218
        self.code = code
 
219
        self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown")
 
220
        self.rtl = False
 
221
        for prefix in ["fa", "ar", "he"]:
 
222
            if self.code.startswith(prefix):
 
223
                self.rtl = True
 
224
                break
 
225
        self.translations = translations
 
226
 
 
227
        # Initialize strings for date formatting
 
228
        _ = self.translate
 
229
        self._months = [
 
230
            _("January"), _("February"), _("March"), _("April"),
 
231
            _("May"), _("June"), _("July"), _("August"),
 
232
            _("September"), _("October"), _("November"), _("December")]
 
233
        self._weekdays = [
 
234
            _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"),
 
235
            _("Friday"), _("Saturday"), _("Sunday")]
 
236
 
 
237
    def translate(self, message, plural_message=None, count=None):
 
238
        raise NotImplementedError()
 
239
 
 
240
    def format_date(self, date, gmt_offset=0, relative=True, shorter=False,
 
241
                    full_format=False):
 
242
        """Formats the given date (which should be GMT).
 
243
 
 
244
        By default, we return a relative time (e.g., "2 minutes ago"). You
 
245
        can return an absolute date string with relative=False.
 
246
 
 
247
        You can force a full format date ("July 10, 1980") with
 
248
        full_format=True.
 
249
        """
 
250
        if self.code.startswith("ru"):
 
251
            relative = False
 
252
        if type(date) in (int, long, float):
 
253
            date = datetime.datetime.utcfromtimestamp(date)
 
254
        now = datetime.datetime.utcnow()
 
255
        # Round down to now. Due to click skew, things are somethings
 
256
        # slightly in the future.
 
257
        if date > now: date = now
 
258
        local_date = date - datetime.timedelta(minutes=gmt_offset)
 
259
        local_now = now - datetime.timedelta(minutes=gmt_offset)
 
260
        local_yesterday = local_now - datetime.timedelta(hours=24)
 
261
        difference = now - date
 
262
        seconds = difference.seconds
 
263
        days = difference.days
 
264
 
 
265
        _ = self.translate
 
266
        format = None
 
267
        if not full_format:
 
268
            if relative and days == 0:
 
269
                if seconds < 50:
 
270
                    return _("1 second ago", "%(seconds)d seconds ago",
 
271
                             seconds) % { "seconds": seconds }
 
272
 
 
273
                if seconds < 50 * 60:
 
274
                    minutes = round(seconds / 60.0)
 
275
                    return _("1 minute ago", "%(minutes)d minutes ago",
 
276
                             minutes) % { "minutes": minutes }
 
277
 
 
278
                hours = round(seconds / (60.0 * 60))
 
279
                return _("1 hour ago", "%(hours)d hours ago",
 
280
                         hours) % { "hours": hours }
 
281
 
 
282
            if days == 0:
 
283
                format = _("%(time)s")
 
284
            elif days == 1 and local_date.day == local_yesterday.day and \
 
285
                 relative:
 
286
                format = _("yesterday") if shorter else \
 
287
                         _("yesterday at %(time)s")
 
288
            elif days < 5:
 
289
                format = _("%(weekday)s") if shorter else \
 
290
                         _("%(weekday)s at %(time)s")
 
291
            elif days < 334:  # 11mo, since confusing for same month last year
 
292
                format = _("%(month_name)s %(day)s") if shorter else \
 
293
                         _("%(month_name)s %(day)s at %(time)s")
 
294
 
 
295
        if format is None:
 
296
            format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
 
297
                     _("%(month_name)s %(day)s, %(year)s at %(time)s")
 
298
 
 
299
        tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
 
300
        if tfhour_clock:
 
301
            str_time = "%d:%02d" % (local_date.hour, local_date.minute)
 
302
        elif self.code == "zh_CN":
 
303
            str_time = "%s%d:%02d" % (
 
304
                (u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12],
 
305
                local_date.hour % 12 or 12, local_date.minute)
 
306
        else:
 
307
            str_time = "%d:%02d %s" % (
 
308
                local_date.hour % 12 or 12, local_date.minute,
 
309
                ("am", "pm")[local_date.hour >= 12])
 
310
 
 
311
        return format % {
 
312
            "month_name": self._months[local_date.month - 1],
 
313
            "weekday": self._weekdays[local_date.weekday()],
 
314
            "day": str(local_date.day),
 
315
            "year": str(local_date.year),
 
316
            "time": str_time
 
317
        }
 
318
 
 
319
    def format_day(self, date, gmt_offset=0, dow=True):
 
320
        """Formats the given date as a day of week.
 
321
 
 
322
        Example: "Monday, January 22". You can remove the day of week with
 
323
        dow=False.
 
324
        """
 
325
        local_date = date - datetime.timedelta(minutes=gmt_offset)
 
326
        _ = self.translate
 
327
        if dow:
 
328
            return _("%(weekday)s, %(month_name)s %(day)s") % {
 
329
                "month_name": self._months[local_date.month - 1],
 
330
                "weekday": self._weekdays[local_date.weekday()],
 
331
                "day": str(local_date.day),
 
332
            }
 
333
        else:
 
334
            return _("%(month_name)s %(day)s") % {
 
335
                "month_name": self._months[local_date.month - 1],
 
336
                "day": str(local_date.day),
 
337
            }
 
338
 
 
339
    def list(self, parts):
 
340
        """Returns a comma-separated list for the given list of parts.
 
341
 
 
342
        The format is, e.g., "A, B and C", "A and B" or just "A" for lists
 
343
        of size 1.
 
344
        """
 
345
        _ = self.translate
 
346
        if len(parts) == 0: return ""
 
347
        if len(parts) == 1: return parts[0]
 
348
        comma = u' \u0648 ' if self.code.startswith("fa") else u", "
 
349
        return _("%(commas)s and %(last)s") % {
 
350
            "commas": comma.join(parts[:-1]),
 
351
            "last": parts[len(parts) - 1],
 
352
        }
 
353
 
 
354
    def friendly_number(self, value):
 
355
        """Returns a comma-separated number for the given integer."""
 
356
        if self.code not in ("en", "en_US"):
 
357
            return str(value)
 
358
        value = str(value)
 
359
        parts = []
 
360
        while value:
 
361
            parts.append(value[-3:])
 
362
            value = value[:-3]
 
363
        return ",".join(reversed(parts))
 
364
 
 
365
class CSVLocale(Locale):
 
366
    """Locale implementation using tornado's CSV translation format."""
 
367
    def translate(self, message, plural_message=None, count=None):
 
368
        """Returns the translation for the given message for this locale.
 
369
 
 
370
        If plural_message is given, you must also provide count. We return
 
371
        plural_message when count != 1, and we return the singular form
 
372
        for the given message when count == 1.
 
373
        """
 
374
        if plural_message is not None:
 
375
            assert count is not None
 
376
            if count != 1:
 
377
                message = plural_message
 
378
                message_dict = self.translations.get("plural", {})
 
379
            else:
 
380
                message_dict = self.translations.get("singular", {})
 
381
        else:
 
382
            message_dict = self.translations.get("unknown", {})
 
383
        return message_dict.get(message, message)
 
384
 
 
385
class GettextLocale(Locale):
 
386
    """Locale implementation using the gettext module."""
 
387
    def translate(self, message, plural_message=None, count=None):
 
388
        if plural_message is not None:
 
389
            assert count is not None
 
390
            return self.translations.ungettext(message, plural_message, count)
 
391
        else:
 
392
            return self.translations.ugettext(message)
 
393
 
 
394
LOCALE_NAMES = {
 
395
    "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},
 
396
    "ar_AR": {"name_en": u"Arabic", "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"},
 
397
    "bg_BG": {"name_en": u"Bulgarian", "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"},
 
398
    "bn_IN": {"name_en": u"Bengali", "name": u"\u09ac\u09be\u0982\u09b2\u09be"},
 
399
    "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"},
 
400
    "ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"},
 
401
    "cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"},
 
402
    "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"},
 
403
    "da_DK": {"name_en": u"Danish", "name": u"Dansk"},
 
404
    "de_DE": {"name_en": u"German", "name": u"Deutsch"},
 
405
    "el_GR": {"name_en": u"Greek", "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"},
 
406
    "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"},
 
407
    "en_US": {"name_en": u"English (US)", "name": u"English (US)"},
 
408
    "es_ES": {"name_en": u"Spanish (Spain)", "name": u"Espa\xf1ol (Espa\xf1a)"},
 
409
    "es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"},
 
410
    "et_EE": {"name_en": u"Estonian", "name": u"Eesti"},
 
411
    "eu_ES": {"name_en": u"Basque", "name": u"Euskara"},
 
412
    "fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"},
 
413
    "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"},
 
414
    "fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"},
 
415
    "fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"},
 
416
    "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"},
 
417
    "gl_ES": {"name_en": u"Galician", "name": u"Galego"},
 
418
    "he_IL": {"name_en": u"Hebrew", "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"},
 
419
    "hi_IN": {"name_en": u"Hindi", "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"},
 
420
    "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"},
 
421
    "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"},
 
422
    "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"},
 
423
    "is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"},
 
424
    "it_IT": {"name_en": u"Italian", "name": u"Italiano"},
 
425
    "ja_JP": {"name_en": u"Japanese", "name": u"\xe6\xe6\xe8"},
 
426
    "ko_KR": {"name_en": u"Korean", "name": u"\xed\xea\xec"},
 
427
    "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"},
 
428
    "lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"},
 
429
    "mk_MK": {"name_en": u"Macedonian", "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"},
 
430
    "ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},
 
431
    "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"},
 
432
    "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"},
 
433
    "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"},
 
434
    "nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"},
 
435
    "pa_IN": {"name_en": u"Punjabi", "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"},
 
436
    "pl_PL": {"name_en": u"Polish", "name": u"Polski"},
 
437
    "pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"},
 
438
    "pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"},
 
439
    "ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"},
 
440
    "ru_RU": {"name_en": u"Russian", "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"},
 
441
    "sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"},
 
442
    "sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"},
 
443
    "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"},
 
444
    "sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"},
 
445
    "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"},
 
446
    "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"},
 
447
    "ta_IN": {"name_en": u"Tamil", "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"},
 
448
    "te_IN": {"name_en": u"Telugu", "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"},
 
449
    "th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},
 
450
    "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"},
 
451
    "tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"},
 
452
    "uk_UA": {"name_en": u"Ukraini ", "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"},
 
453
    "vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"},
 
454
    "zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"\xe4\xe6(\xe7\xe4)"},
 
455
    "zh_HK": {"name_en": u"Chinese (Hong Kong)", "name": u"\xe4\xe6(\xe9\xe6)"},
 
456
    "zh_TW": {"name_en": u"Chinese (Taiwan)", "name": u"\xe4\xe6(\xe5\xe7)"},
 
457
}