babel-2.14.0/0000755000175000017500000000000014536056757012213 5ustar nileshnileshbabel-2.14.0/tests/0000755000175000017500000000000014536056757013355 5ustar nileshnileshbabel-2.14.0/tests/test_localedata.py0000644000175000017500000001160114536056757017056 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import os import pickle import random import sys import tempfile import unittest from operator import methodcaller import pytest from babel import Locale, UnknownLocaleError, localedata class MergeResolveTestCase(unittest.TestCase): def test_merge_items(self): d = {1: 'foo', 3: 'baz'} localedata.merge(d, {1: 'Foo', 2: 'Bar'}) assert d == {1: 'Foo', 2: 'Bar', 3: 'baz'} def test_merge_nested_dict(self): d1 = {'x': {'a': 1, 'b': 2, 'c': 3}} d2 = {'x': {'a': 1, 'b': 12, 'd': 14}} localedata.merge(d1, d2) assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}} def test_merge_nested_dict_no_overlap(self): d1 = {'x': {'a': 1, 'b': 2}} d2 = {'y': {'a': 11, 'b': 12}} localedata.merge(d1, d2) assert d1 == {'x': {'a': 1, 'b': 2}, 'y': {'a': 11, 'b': 12}} def test_merge_with_alias_and_resolve(self): alias = localedata.Alias('x') d1 = { 'x': {'a': 1, 'b': 2, 'c': 3}, 'y': alias, } d2 = { 'x': {'a': 1, 'b': 12, 'd': 14}, 'y': {'b': 22, 'e': 25}, } localedata.merge(d1, d2) assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': (alias, {'b': 22, 'e': 25})} d = localedata.LocaleDataDict(d1) assert dict(d.items()) == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25}} def test_load(): assert localedata.load('en_US')['languages']['sv'] == 'Swedish' assert localedata.load('en_US') is localedata.load('en_US') def test_merge(): d = {1: 'foo', 3: 'baz'} localedata.merge(d, {1: 'Foo', 2: 'Bar'}) assert d == {1: 'Foo', 2: 'Bar', 3: 'baz'} def test_locale_identification(): for locale in localedata.locale_identifiers(): assert localedata.exists(locale) def test_unique_ids(): # Check all locale IDs are uniques. all_ids = localedata.locale_identifiers() assert len(all_ids) == len(set(all_ids)) # Check locale IDs don't collide after lower-case normalization. lower_case_ids = list(map(methodcaller('lower'), all_ids)) assert len(lower_case_ids) == len(set(lower_case_ids)) def test_mixedcased_locale(): for locale in localedata.locale_identifiers(): locale_id = ''.join([ methodcaller(random.choice(['lower', 'upper']))(c) for c in locale]) assert localedata.exists(locale_id) def test_locale_argument_acceptance(): # Testing None input. normalized_locale = localedata.normalize_locale(None) assert normalized_locale is None assert not localedata.exists(None) # Testing list input. normalized_locale = localedata.normalize_locale(['en_us', None]) assert normalized_locale is None assert not localedata.exists(['en_us', None]) def test_locale_identifiers_cache(monkeypatch): original_listdir = localedata.os.listdir listdir_calls = [] def listdir_spy(*args): rv = original_listdir(*args) listdir_calls.append((args, rv)) return rv monkeypatch.setattr(localedata.os, 'listdir', listdir_spy) localedata.locale_identifiers.cache_clear() assert not listdir_calls l = localedata.locale_identifiers() assert len(listdir_calls) == 1 assert localedata.locale_identifiers() is l assert len(listdir_calls) == 1 localedata.locale_identifiers.cache_clear() assert localedata.locale_identifiers() assert len(listdir_calls) == 2 def test_locale_name_cleanup(): """ Test that locale identifiers are cleaned up to avoid directory traversal. """ no_exist_name = os.path.join(tempfile.gettempdir(), "babel%d.dat" % random.randint(1, 99999)) with open(no_exist_name, "wb") as f: pickle.dump({}, f) try: name = os.path.splitext(os.path.relpath(no_exist_name, localedata._dirname))[0] except ValueError: if sys.platform == "win32": pytest.skip("unable to form relpath") raise assert not localedata.exists(name) with pytest.raises(IOError): localedata.load(name) with pytest.raises(UnknownLocaleError): Locale(name) @pytest.mark.skipif(sys.platform != "win32", reason="windows-only test") def test_reserved_locale_names(): for name in ("con", "aux", "nul", "prn", "com8", "lpt5"): with pytest.raises(ValueError): localedata.load(name) with pytest.raises(ValueError): Locale(name) babel-2.14.0/tests/test_lists.py0000644000175000017500000000130314536056757016121 0ustar nileshnileshimport pytest from babel import lists def test_format_list(): for list, locale, expected in [ ([], 'en', ''), (['string'], 'en', 'string'), (['string1', 'string2'], 'en', 'string1 and string2'), (['string1', 'string2', 'string3'], 'en', 'string1, string2, and string3'), (['string1', 'string2', 'string3'], 'zh', 'string1、string2和string3'), (['string1', 'string2', 'string3', 'string4'], 'ne', 'string1,string2, string3 र string4'), ]: assert lists.format_list(list, locale=locale) == expected def test_format_list_error(): with pytest.raises(ValueError): lists.format_list(['a', 'b', 'c'], style='orange', locale='en') babel-2.14.0/tests/test_plural.py0000644000175000017500000002257514536056757016300 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import decimal import unittest import pytest from babel import localedata, plural EPSILON = decimal.Decimal("0.0001") def test_plural_rule(): rule = plural.PluralRule({'one': 'n is 1'}) assert rule(1) == 'one' assert rule(2) == 'other' rule = plural.PluralRule({'one': 'n is 1'}) assert rule.rules == {'one': 'n is 1'} def test_plural_rule_operands_i(): rule = plural.PluralRule({'one': 'i is 1'}) assert rule(1.2) == 'one' assert rule(2) == 'other' def test_plural_rule_operands_v(): rule = plural.PluralRule({'one': 'v is 2'}) assert rule(decimal.Decimal('1.20')) == 'one' assert rule(decimal.Decimal('1.2')) == 'other' assert rule(2) == 'other' def test_plural_rule_operands_w(): rule = plural.PluralRule({'one': 'w is 2'}) assert rule(decimal.Decimal('1.23')) == 'one' assert rule(decimal.Decimal('1.20')) == 'other' assert rule(1.2) == 'other' def test_plural_rule_operands_f(): rule = plural.PluralRule({'one': 'f is 20'}) assert rule(decimal.Decimal('1.23')) == 'other' assert rule(decimal.Decimal('1.20')) == 'one' assert rule(1.2) == 'other' def test_plural_rule_operands_t(): rule = plural.PluralRule({'one': 't = 5'}) assert rule(decimal.Decimal('1.53')) == 'other' assert rule(decimal.Decimal('1.50')) == 'one' assert rule(1.5) == 'one' def test_plural_other_is_ignored(): rule = plural.PluralRule({'one': 'n is 1', 'other': '@integer 2'}) assert rule(1) == 'one' def test_to_javascript(): assert (plural.to_javascript({'one': 'n is 1'}) == "(function(n) { return (n == 1) ? 'one' : 'other'; })") def test_to_python(): func = plural.to_python({'one': 'n is 1', 'few': 'n in 2..4'}) assert func(1) == 'one' assert func(3) == 'few' func = plural.to_python({'one': 'n in 1,11', 'few': 'n in 3..10,13..19'}) assert func(11) == 'one' assert func(15) == 'few' def test_to_gettext(): assert (plural.to_gettext({'one': 'n is 1', 'two': 'n is 2'}) == 'nplurals=3; plural=((n == 1) ? 0 : (n == 2) ? 1 : 2);') def test_in_range_list(): assert plural.in_range_list(1, [(1, 3)]) assert plural.in_range_list(3, [(1, 3)]) assert plural.in_range_list(3, [(1, 3), (5, 8)]) assert not plural.in_range_list(1.2, [(1, 4)]) assert not plural.in_range_list(10, [(1, 4)]) assert not plural.in_range_list(10, [(1, 4), (6, 8)]) def test_within_range_list(): assert plural.within_range_list(1, [(1, 3)]) assert plural.within_range_list(1.0, [(1, 3)]) assert plural.within_range_list(1.2, [(1, 4)]) assert plural.within_range_list(8.8, [(1, 4), (7, 15)]) assert not plural.within_range_list(10, [(1, 4)]) assert not plural.within_range_list(10.5, [(1, 4), (20, 30)]) def test_cldr_modulo(): assert plural.cldr_modulo(-3, 5) == -3 assert plural.cldr_modulo(-3, -5) == -3 assert plural.cldr_modulo(3, 5) == 3 def test_plural_within_rules(): p = plural.PluralRule({'one': 'n is 1', 'few': 'n within 2,4,7..9'}) assert repr(p) == "" assert plural.to_javascript(p) == ( "(function(n) { " "return ((n == 2) || (n == 4) || (n >= 7 && n <= 9))" " ? 'few' : (n == 1) ? 'one' : 'other'; })") assert plural.to_gettext(p) == ( 'nplurals=3; plural=(((n == 2) || (n == 4) || (n >= 7 && n <= 9))' ' ? 1 : (n == 1) ? 0 : 2);') assert p(0) == 'other' assert p(1) == 'one' assert p(2) == 'few' assert p(3) == 'other' assert p(4) == 'few' assert p(5) == 'other' assert p(6) == 'other' assert p(7) == 'few' assert p(8) == 'few' assert p(9) == 'few' def test_locales_with_no_plural_rules_have_default(): from babel import Locale pf = Locale.parse('ii').plural_form assert pf(1) == 'other' assert pf(2) == 'other' assert pf(15) == 'other' WELL_FORMED_TOKEN_TESTS = ( ("", []), ( "n = 1", [ ("value", "1"), ("symbol", "="), ("word", "n"), ], ), ( "n = 1 @integer 1", [ ("value", "1"), ("symbol", "="), ("word", "n"), ], ), ( "n is 1", [ ("value", "1"), ("word", "is"), ("word", "n"), ], ), ( "n % 100 = 3..10", [ ("value", "10"), ("ellipsis", ".."), ("value", "3"), ("symbol", "="), ("value", "100"), ("symbol", "%"), ("word", "n"), ], ), ) @pytest.mark.parametrize('rule_text,tokens', WELL_FORMED_TOKEN_TESTS) def test_tokenize_well_formed(rule_text, tokens): assert plural.tokenize_rule(rule_text) == tokens MALFORMED_TOKEN_TESTS = ( 'a = 1', 'n ! 2', ) @pytest.mark.parametrize('rule_text', MALFORMED_TOKEN_TESTS) def test_tokenize_malformed(rule_text): with pytest.raises(plural.RuleError): plural.tokenize_rule(rule_text) class TestNextTokenTestCase(unittest.TestCase): def test_empty(self): assert not plural.test_next_token([], '') def test_type_ok_and_no_value(self): assert plural.test_next_token([('word', 'and')], 'word') def test_type_ok_and_not_value(self): assert not plural.test_next_token([('word', 'and')], 'word', 'or') def test_type_ok_and_value_ok(self): assert plural.test_next_token([('word', 'and')], 'word', 'and') def test_type_not_ok_and_value_ok(self): assert not plural.test_next_token([('abc', 'and')], 'word', 'and') def make_range_list(*values): ranges = [] for v in values: if isinstance(v, int): val_node = plural.value_node(v) ranges.append((val_node, val_node)) else: assert isinstance(v, tuple) ranges.append((plural.value_node(v[0]), plural.value_node(v[1]))) return plural.range_list_node(ranges) class PluralRuleParserTestCase(unittest.TestCase): def setUp(self): self.n = plural.ident_node('n') def n_eq(self, v): return 'relation', ('in', self.n, make_range_list(v)) def test_error_when_unexpected_end(self): with pytest.raises(plural.RuleError): plural._Parser('n =') def test_eq_relation(self): assert plural._Parser('n = 1').ast == self.n_eq(1) def test_in_range_relation(self): assert plural._Parser('n = 2..4').ast == \ ('relation', ('in', self.n, make_range_list((2, 4)))) def test_negate(self): assert plural._Parser('n != 1').ast == plural.negate(self.n_eq(1)) def test_or(self): assert plural._Parser('n = 1 or n = 2').ast ==\ ('or', (self.n_eq(1), self.n_eq(2))) def test_and(self): assert plural._Parser('n = 1 and n = 2').ast ==\ ('and', (self.n_eq(1), self.n_eq(2))) def test_or_and(self): assert plural._Parser('n = 0 or n != 1 and n % 100 = 1..19').ast == \ ('or', (self.n_eq(0), ('and', (plural.negate(self.n_eq(1)), ('relation', ('in', ('mod', (self.n, plural.value_node(100))), (make_range_list((1, 19))))))), )) EXTRACT_OPERANDS_TESTS = ( (1, 1, 1, 0, 0, 0, 0), (decimal.Decimal('1.0'), '1.0', 1, 1, 0, 0, 0), (decimal.Decimal('1.00'), '1.00', 1, 2, 0, 0, 0), (decimal.Decimal('1.3'), '1.3', 1, 1, 1, 3, 3), (decimal.Decimal('1.30'), '1.30', 1, 2, 1, 30, 3), (decimal.Decimal('1.03'), '1.03', 1, 2, 2, 3, 3), (decimal.Decimal('1.230'), '1.230', 1, 3, 2, 230, 23), (-1, 1, 1, 0, 0, 0, 0), (1.3, '1.3', 1, 1, 1, 3, 3), ) @pytest.mark.parametrize('source,n,i,v,w,f,t', EXTRACT_OPERANDS_TESTS) def test_extract_operands(source, n, i, v, w, f, t): e_n, e_i, e_v, e_w, e_f, e_t, e_c, e_e = plural.extract_operands(source) assert abs(e_n - decimal.Decimal(n)) <= EPSILON # float-decimal conversion inaccuracy assert e_i == i assert e_v == v assert e_w == w assert e_f == f assert e_t == t assert not e_c # Not supported at present assert not e_e # Not supported at present @pytest.mark.parametrize('locale', ('ru', 'pl')) def test_gettext_compilation(locale): # Test that new plural form elements introduced in recent CLDR versions # are compiled "down" to `n` when emitting Gettext rules. ru_rules = localedata.load(locale)['plural_form'].rules chars = 'ivwft' # Test that these rules are valid for this test; i.e. that they contain at least one # of the gettext-unsupported characters. assert any(f" {ch} " in rule for ch in chars for rule in ru_rules.values()) # Then test that the generated value indeed does not contain these. ru_rules_gettext = plural.to_gettext(ru_rules) assert not any(ch in ru_rules_gettext for ch in chars) babel-2.14.0/tests/test_support.py0000644000175000017500000003714514536056757016514 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import datetime import inspect import os import shutil import sys import tempfile import unittest from decimal import Decimal from io import BytesIO import pytest from babel import support from babel.messages import Catalog from babel.messages.mofile import write_mo SKIP_LGETTEXT = sys.version_info >= (3, 8) @pytest.mark.usefixtures("os_environ") class TranslationsTestCase(unittest.TestCase): def setUp(self): # Use a locale which won't fail to run the tests os.environ['LANG'] = 'en_US.UTF-8' messages1 = [ ('foo', {'string': 'Voh'}), ('foo', {'string': 'VohCTX', 'context': 'foo'}), (('foo1', 'foos1'), {'string': ('Voh1', 'Vohs1')}), (('foo1', 'foos1'), {'string': ('VohCTX1', 'VohsCTX1'), 'context': 'foo'}), ] messages2 = [ ('foo', {'string': 'VohD'}), ('foo', {'string': 'VohCTXD', 'context': 'foo'}), (('foo1', 'foos1'), {'string': ('VohD1', 'VohsD1')}), (('foo1', 'foos1'), {'string': ('VohCTXD1', 'VohsCTXD1'), 'context': 'foo'}), ] catalog1 = Catalog(locale='en_GB', domain='messages') catalog2 = Catalog(locale='en_GB', domain='messages1') for ids, kwargs in messages1: catalog1.add(ids, **kwargs) for ids, kwargs in messages2: catalog2.add(ids, **kwargs) catalog1_fp = BytesIO() catalog2_fp = BytesIO() write_mo(catalog1_fp, catalog1) catalog1_fp.seek(0) write_mo(catalog2_fp, catalog2) catalog2_fp.seek(0) translations1 = support.Translations(catalog1_fp) translations2 = support.Translations(catalog2_fp, domain='messages1') self.translations = translations1.add(translations2, merge=False) def assertEqualTypeToo(self, expected, result): assert expected == result assert type(expected) == type(result), f"instance types do not match: {type(expected)!r}!={type(result)!r}" def test_pgettext(self): self.assertEqualTypeToo('Voh', self.translations.gettext('foo')) self.assertEqualTypeToo('VohCTX', self.translations.pgettext('foo', 'foo')) def test_upgettext(self): self.assertEqualTypeToo('Voh', self.translations.ugettext('foo')) self.assertEqualTypeToo('VohCTX', self.translations.upgettext('foo', 'foo')) @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_lpgettext(self): self.assertEqualTypeToo(b'Voh', self.translations.lgettext('foo')) self.assertEqualTypeToo(b'VohCTX', self.translations.lpgettext('foo', 'foo')) def test_npgettext(self): self.assertEqualTypeToo('Voh1', self.translations.ngettext('foo1', 'foos1', 1)) self.assertEqualTypeToo('Vohs1', self.translations.ngettext('foo1', 'foos1', 2)) self.assertEqualTypeToo('VohCTX1', self.translations.npgettext('foo', 'foo1', 'foos1', 1)) self.assertEqualTypeToo('VohsCTX1', self.translations.npgettext('foo', 'foo1', 'foos1', 2)) def test_unpgettext(self): self.assertEqualTypeToo('Voh1', self.translations.ungettext('foo1', 'foos1', 1)) self.assertEqualTypeToo('Vohs1', self.translations.ungettext('foo1', 'foos1', 2)) self.assertEqualTypeToo('VohCTX1', self.translations.unpgettext('foo', 'foo1', 'foos1', 1)) self.assertEqualTypeToo('VohsCTX1', self.translations.unpgettext('foo', 'foo1', 'foos1', 2)) @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_lnpgettext(self): self.assertEqualTypeToo(b'Voh1', self.translations.lngettext('foo1', 'foos1', 1)) self.assertEqualTypeToo(b'Vohs1', self.translations.lngettext('foo1', 'foos1', 2)) self.assertEqualTypeToo(b'VohCTX1', self.translations.lnpgettext('foo', 'foo1', 'foos1', 1)) self.assertEqualTypeToo(b'VohsCTX1', self.translations.lnpgettext('foo', 'foo1', 'foos1', 2)) def test_dpgettext(self): self.assertEqualTypeToo( 'VohD', self.translations.dgettext('messages1', 'foo')) self.assertEqualTypeToo( 'VohCTXD', self.translations.dpgettext('messages1', 'foo', 'foo')) def test_dupgettext(self): self.assertEqualTypeToo( 'VohD', self.translations.dugettext('messages1', 'foo')) self.assertEqualTypeToo( 'VohCTXD', self.translations.dupgettext('messages1', 'foo', 'foo')) @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_ldpgettext(self): self.assertEqualTypeToo( b'VohD', self.translations.ldgettext('messages1', 'foo')) self.assertEqualTypeToo( b'VohCTXD', self.translations.ldpgettext('messages1', 'foo', 'foo')) def test_dnpgettext(self): self.assertEqualTypeToo( 'VohD1', self.translations.dngettext('messages1', 'foo1', 'foos1', 1)) self.assertEqualTypeToo( 'VohsD1', self.translations.dngettext('messages1', 'foo1', 'foos1', 2)) self.assertEqualTypeToo( 'VohCTXD1', self.translations.dnpgettext('messages1', 'foo', 'foo1', 'foos1', 1)) self.assertEqualTypeToo( 'VohsCTXD1', self.translations.dnpgettext('messages1', 'foo', 'foo1', 'foos1', 2)) def test_dunpgettext(self): self.assertEqualTypeToo( 'VohD1', self.translations.dungettext('messages1', 'foo1', 'foos1', 1)) self.assertEqualTypeToo( 'VohsD1', self.translations.dungettext('messages1', 'foo1', 'foos1', 2)) self.assertEqualTypeToo( 'VohCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1', 'foos1', 1)) self.assertEqualTypeToo( 'VohsCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1', 'foos1', 2)) @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_ldnpgettext(self): self.assertEqualTypeToo( b'VohD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 1)) self.assertEqualTypeToo( b'VohsD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 2)) self.assertEqualTypeToo( b'VohCTXD1', self.translations.ldnpgettext('messages1', 'foo', 'foo1', 'foos1', 1)) self.assertEqualTypeToo( b'VohsCTXD1', self.translations.ldnpgettext('messages1', 'foo', 'foo1', 'foos1', 2)) def test_load(self): tempdir = tempfile.mkdtemp() try: messages_dir = os.path.join(tempdir, 'fr', 'LC_MESSAGES') os.makedirs(messages_dir) catalog = Catalog(locale='fr', domain='messages') catalog.add('foo', 'bar') with open(os.path.join(messages_dir, 'messages.mo'), 'wb') as f: write_mo(f, catalog) translations = support.Translations.load(tempdir, locales=('fr',), domain='messages') assert translations.gettext('foo') == 'bar' finally: shutil.rmtree(tempdir) class NullTranslationsTestCase(unittest.TestCase): def setUp(self): fp = BytesIO() write_mo(fp, Catalog(locale='de')) fp.seek(0) self.translations = support.Translations(fp=fp) self.null_translations = support.NullTranslations(fp=fp) def method_names(self): names = [name for name in dir(self.translations) if 'gettext' in name] if SKIP_LGETTEXT: # Remove deprecated l*gettext functions names = [name for name in names if not name.startswith('l')] return names def test_same_methods(self): for name in self.method_names(): if not hasattr(self.null_translations, name): self.fail(f"NullTranslations does not provide method {name!r}") def test_method_signature_compatibility(self): for name in self.method_names(): translations_method = getattr(self.translations, name) null_method = getattr(self.null_translations, name) assert inspect.getfullargspec(translations_method) == inspect.getfullargspec(null_method) def test_same_return_values(self): data = { 'message': 'foo', 'domain': 'domain', 'context': 'tests', 'singular': 'bar', 'plural': 'baz', 'num': 1, 'msgid1': 'bar', 'msgid2': 'baz', 'n': 1, } for name in self.method_names(): method = getattr(self.translations, name) null_method = getattr(self.null_translations, name) signature = inspect.getfullargspec(method) parameter_names = [name for name in signature.args if name != 'self'] values = [data[name] for name in parameter_names] assert method(*values) == null_method(*values) class LazyProxyTestCase(unittest.TestCase): def test_proxy_caches_result_of_function_call(self): self.counter = 0 def add_one(): self.counter += 1 return self.counter proxy = support.LazyProxy(add_one) assert proxy.value == 1 assert proxy.value == 1 def test_can_disable_proxy_cache(self): self.counter = 0 def add_one(): self.counter += 1 return self.counter proxy = support.LazyProxy(add_one, enable_cache=False) assert proxy.value == 1 assert proxy.value == 2 def test_can_copy_proxy(self): from copy import copy numbers = [1, 2] def first(xs): return xs[0] proxy = support.LazyProxy(first, numbers) proxy_copy = copy(proxy) numbers.pop(0) assert proxy.value == 2 assert proxy_copy.value == 2 def test_can_deepcopy_proxy(self): from copy import deepcopy numbers = [1, 2] def first(xs): return xs[0] proxy = support.LazyProxy(first, numbers) proxy_deepcopy = deepcopy(proxy) numbers.pop(0) assert proxy.value == 2 assert proxy_deepcopy.value == 1 def test_handle_attribute_error(self): def raise_attribute_error(): raise AttributeError('message') proxy = support.LazyProxy(raise_attribute_error) with pytest.raises(AttributeError) as exception: _ = proxy.value assert str(exception.value) == 'message' class TestFormat: def test_format_datetime(self, timezone_getter): when = datetime.datetime(2007, 4, 1, 15, 30) fmt = support.Format('en_US', tzinfo=timezone_getter('US/Eastern')) assert fmt.datetime(when) == 'Apr 1, 2007, 11:30:00\u202fAM' def test_format_time(self, timezone_getter): when = datetime.datetime(2007, 4, 1, 15, 30) fmt = support.Format('en_US', tzinfo=timezone_getter('US/Eastern')) assert fmt.time(when) == '11:30:00\u202fAM' def test_format_number(self): assert support.Format('en_US').number(1234) == '1,234' assert support.Format('ar_EG', numbering_system="default").number(1234) == '1٬234' def test_format_decimal(self): assert support.Format('en_US').decimal(1234.5) == '1,234.5' assert support.Format('en_US').decimal(Decimal("1234.5")) == '1,234.5' assert support.Format('ar_EG', numbering_system="default").decimal(1234.5) == '1٬234٫5' assert support.Format('ar_EG', numbering_system="default").decimal(Decimal("1234.5")) == '1٬234٫5' def test_format_compact_decimal(self): assert support.Format('en_US').compact_decimal(1234) == '1K' assert support.Format('ar_EG', numbering_system="default").compact_decimal( 1234, fraction_digits=1) == '1٫2\xa0ألف' assert support.Format('ar_EG', numbering_system="default").compact_decimal( Decimal("1234"), fraction_digits=1) == '1٫2\xa0ألف' def test_format_currency(self): assert support.Format('en_US').currency(1099.98, 'USD') == '$1,099.98' assert support.Format('en_US').currency(Decimal("1099.98"), 'USD') == '$1,099.98' assert support.Format('ar_EG', numbering_system="default").currency( 1099.98, 'EGP') == '\u200f1٬099٫98\xa0ج.م.\u200f' def test_format_compact_currency(self): assert support.Format('en_US').compact_currency(1099.98, 'USD') == '$1K' assert support.Format('en_US').compact_currency(Decimal("1099.98"), 'USD') == '$1K' assert support.Format('ar_EG', numbering_system="default").compact_currency( 1099.98, 'EGP') == '1\xa0ألف\xa0ج.م.\u200f' def test_format_percent(self): assert support.Format('en_US').percent(0.34) == '34%' assert support.Format('en_US').percent(Decimal("0.34")) == '34%' assert support.Format('ar_EG', numbering_system="default").percent(134.5) == '13٬450%' def test_format_scientific(self): assert support.Format('en_US').scientific(10000) == '1E4' assert support.Format('en_US').scientific(Decimal("10000")) == '1E4' assert support.Format('ar_EG', numbering_system="default").scientific(10000) == '1اس4' def test_lazy_proxy(): def greeting(name='world'): return f"Hello, {name}!" lazy_greeting = support.LazyProxy(greeting, name='Joe') assert str(lazy_greeting) == "Hello, Joe!" assert ' ' + lazy_greeting == ' Hello, Joe!' assert '(%s)' % lazy_greeting == '(Hello, Joe!)' assert f"[{lazy_greeting}]" == "[Hello, Joe!]" greetings = sorted([ support.LazyProxy(greeting, 'world'), support.LazyProxy(greeting, 'Joe'), support.LazyProxy(greeting, 'universe'), ]) assert [str(g) for g in greetings] == [ "Hello, Joe!", "Hello, universe!", "Hello, world!", ] def test_catalog_merge_files(): # Refs issues #92, #162 t1 = support.Translations() assert t1.files == [] t1._catalog["foo"] = "bar" fp = BytesIO() write_mo(fp, Catalog()) fp.seek(0) fp.name = "pro.mo" t2 = support.Translations(fp) assert t2.files == ["pro.mo"] t2._catalog["bar"] = "quux" t1.merge(t2) assert t1.files == ["pro.mo"] assert set(t1._catalog.keys()) == {'', 'foo', 'bar'} babel-2.14.0/tests/test_day_periods.py0000644000175000017500000000144414536056757017273 0ustar nileshnileshfrom datetime import time import pytest import babel.dates as dates @pytest.mark.parametrize("locale, time, expected_period_id", [ ("de", time(7, 42), "morning1"), # (from, before) ("de", time(3, 11), "night1"), # (after, before) ("fi", time(0), "midnight"), # (at) ("en_US", time(12), "noon"), # (at) ("en_US", time(21), "night1"), # (from, before) across 0:00 ("en_US", time(5), "night1"), # (from, before) across 0:00 ("en_US", time(6), "morning1"), # (from, before) ("agq", time(10), "am"), # no periods defined ("agq", time(22), "pm"), # no periods defined ("am", time(14), "afternoon1"), # (before, after) ]) def test_day_period_rules(locale, time, expected_period_id): assert dates.get_period_id(time, locale=locale) == expected_period_id babel-2.14.0/tests/test_dates.py0000644000175000017500000007564214536056757016104 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import calendar from datetime import date, datetime, time, timedelta import freezegun import pytest from babel import Locale, dates from babel.dates import NO_INHERITANCE_MARKER, UTC, _localize from babel.util import FixedOffsetTimezone class DateTimeFormatTestCase: def test_quarter_format(self): d = date(2006, 6, 8) fmt = dates.DateTimeFormat(d, locale='en_US') assert fmt['Q'] == '2' assert fmt['QQQQ'] == '2nd quarter' assert fmt['q'] == '2' assert fmt['qqqq'] == '2nd quarter' d = date(2006, 12, 31) fmt = dates.DateTimeFormat(d, locale='en_US') assert fmt['qqq'] == 'Q4' assert fmt['qqqqq'] == '4' assert fmt['QQQ'] == 'Q4' assert fmt['QQQQQ'] == '4' def test_month_context(self): d = date(2006, 2, 8) assert dates.DateTimeFormat(d, locale='mt_MT')['MMMMM'] == 'F' # narrow format assert dates.DateTimeFormat(d, locale='mt_MT')['LLLLL'] == 'Fr' # narrow standalone def test_abbreviated_month_alias(self): d = date(2006, 3, 8) assert dates.DateTimeFormat(d, locale='de_DE')['LLL'] == 'Mär' def test_week_of_year_first(self): d = date(2006, 1, 8) assert dates.DateTimeFormat(d, locale='de_DE')['w'] == '1' assert dates.DateTimeFormat(d, locale='en_US')['ww'] == '02' def test_week_of_year_first_with_year(self): d = date(2006, 1, 1) fmt = dates.DateTimeFormat(d, locale='de_DE') assert fmt['w'] == '52' assert fmt['YYYY'] == '2005' def test_week_of_year_last(self): d = date(2006, 12, 26) assert dates.DateTimeFormat(d, locale='de_DE')['w'] == '52' assert dates.DateTimeFormat(d, locale='en_US')['w'] == '52' def test_week_of_year_last_us_extra_week(self): d = date(2005, 12, 26) assert dates.DateTimeFormat(d, locale='de_DE')['w'] == '52' assert dates.DateTimeFormat(d, locale='en_US')['w'] == '53' def test_week_of_year_de_first_us_last_with_year(self): d = date(2018, 12, 31) fmt = dates.DateTimeFormat(d, locale='de_DE') assert fmt['w'] == '1' assert fmt['YYYY'] == '2019' fmt = dates.DateTimeFormat(d, locale='en_US') assert fmt['w'] == '53' assert fmt['yyyy'] == '2018' def test_week_of_month_first(self): d = date(2006, 1, 8) assert dates.DateTimeFormat(d, locale='de_DE')['W'] == '1' assert dates.DateTimeFormat(d, locale='en_US')['W'] == '2' def test_week_of_month_last(self): d = date(2006, 1, 29) assert dates.DateTimeFormat(d, locale='de_DE')['W'] == '4' assert dates.DateTimeFormat(d, locale='en_US')['W'] == '5' def test_day_of_year(self): d = date(2007, 4, 1) assert dates.DateTimeFormat(d, locale='en_US')['D'] == '91' def test_day_of_year_works_with_datetime(self): d = datetime(2007, 4, 1) assert dates.DateTimeFormat(d, locale='en_US')['D'] == '91' def test_day_of_year_first(self): d = date(2007, 1, 1) assert dates.DateTimeFormat(d, locale='en_US')['DDD'] == '001' def test_day_of_year_last(self): d = date(2007, 12, 31) assert dates.DateTimeFormat(d, locale='en_US')['DDD'] == '365' def test_day_of_week_in_month(self): d = date(2007, 4, 15) assert dates.DateTimeFormat(d, locale='en_US')['F'] == '3' def test_day_of_week_in_month_first(self): d = date(2007, 4, 1) assert dates.DateTimeFormat(d, locale='en_US')['F'] == '1' def test_day_of_week_in_month_last(self): d = date(2007, 4, 29) assert dates.DateTimeFormat(d, locale='en_US')['F'] == '5' def test_local_day_of_week(self): d = date(2007, 4, 1) # a sunday assert dates.DateTimeFormat(d, locale='de_DE')['e'] == '7' # monday is first day of week assert dates.DateTimeFormat(d, locale='en_US')['ee'] == '01' # sunday is first day of week assert dates.DateTimeFormat(d, locale='ar_BH')['ee'] == '02' # saturday is first day of week d = date(2007, 4, 2) # a monday assert dates.DateTimeFormat(d, locale='de_DE')['e'] == '1' # monday is first day of week assert dates.DateTimeFormat(d, locale='en_US')['ee'] == '02' # sunday is first day of week assert dates.DateTimeFormat(d, locale='ar_BH')['ee'] == '03' # saturday is first day of week def test_local_day_of_week_standalone(self): d = date(2007, 4, 1) # a sunday assert dates.DateTimeFormat(d, locale='de_DE')['c'] == '7' # monday is first day of week assert dates.DateTimeFormat(d, locale='en_US')['c'] == '1' # sunday is first day of week assert dates.DateTimeFormat(d, locale='ar_BH')['c'] == '2' # saturday is first day of week d = date(2007, 4, 2) # a monday assert dates.DateTimeFormat(d, locale='de_DE')['c'] == '1' # monday is first day of week assert dates.DateTimeFormat(d, locale='en_US')['c'] == '2' # sunday is first day of week assert dates.DateTimeFormat(d, locale='ar_BH')['c'] == '3' # saturday is first day of week def test_pattern_day_of_week(self): dt = datetime(2016, 2, 6) fmt = dates.DateTimeFormat(dt, locale='en_US') assert fmt['c'] == '7' assert fmt['ccc'] == 'Sat' assert fmt['cccc'] == 'Saturday' assert fmt['ccccc'] == 'S' assert fmt['cccccc'] == 'Sa' assert fmt['e'] == '7' assert fmt['ee'] == '07' assert fmt['eee'] == 'Sat' assert fmt['eeee'] == 'Saturday' assert fmt['eeeee'] == 'S' assert fmt['eeeeee'] == 'Sa' assert fmt['E'] == 'Sat' assert fmt['EE'] == 'Sat' assert fmt['EEE'] == 'Sat' assert fmt['EEEE'] == 'Saturday' assert fmt['EEEEE'] == 'S' assert fmt['EEEEEE'] == 'Sa' fmt = dates.DateTimeFormat(dt, locale='uk') assert fmt['c'] == '6' assert fmt['e'] == '6' assert fmt['ee'] == '06' def test_fractional_seconds(self): t = time(8, 3, 9, 799) assert dates.DateTimeFormat(t, locale='en_US')['S'] == '0' t = time(8, 3, 1, 799) assert dates.DateTimeFormat(t, locale='en_US')['SSSS'] == '0008' t = time(8, 3, 1, 34567) assert dates.DateTimeFormat(t, locale='en_US')['SSSS'] == '0346' t = time(8, 3, 1, 345678) assert dates.DateTimeFormat(t, locale='en_US')['SSSSSS'] == '345678' t = time(8, 3, 1, 799) assert dates.DateTimeFormat(t, locale='en_US')['SSSSS'] == '00080' def test_fractional_seconds_zero(self): t = time(15, 30, 0) assert dates.DateTimeFormat(t, locale='en_US')['SSSS'] == '0000' def test_milliseconds_in_day(self): t = time(15, 30, 12, 345000) assert dates.DateTimeFormat(t, locale='en_US')['AAAA'] == '55812345' def test_milliseconds_in_day_zero(self): d = time(0, 0, 0) assert dates.DateTimeFormat(d, locale='en_US')['AAAA'] == '0000' def test_timezone_rfc822(self, timezone_getter): tz = timezone_getter('Europe/Berlin') t = _localize(tz, datetime(2015, 1, 1, 15, 30)) assert dates.DateTimeFormat(t, locale='de_DE')['Z'] == '+0100' def test_timezone_gmt(self, timezone_getter): tz = timezone_getter('Europe/Berlin') t = _localize(tz, datetime(2015, 1, 1, 15, 30)) assert dates.DateTimeFormat(t, locale='de_DE')['ZZZZ'] == 'GMT+01:00' def test_timezone_name(self, timezone_getter): tz = timezone_getter('Europe/Paris') dt = _localize(tz, datetime(2007, 4, 1, 15, 30)) assert dates.DateTimeFormat(dt, locale='fr_FR')['v'] == 'heure : France' def test_timezone_location_format(self, timezone_getter): tz = timezone_getter('Europe/Paris') dt = _localize(tz, datetime(2007, 4, 1, 15, 30)) assert dates.DateTimeFormat(dt, locale='fr_FR')['VVVV'] == 'heure : France' def test_timezone_walltime_short(self, timezone_getter): tz = timezone_getter('Europe/Paris') t = time(15, 30, tzinfo=tz) assert dates.DateTimeFormat(t, locale='fr_FR')['v'] == 'heure : France' def test_timezone_walltime_long(self, timezone_getter): tz = timezone_getter('Europe/Paris') t = time(15, 30, tzinfo=tz) assert dates.DateTimeFormat(t, locale='fr_FR')['vvvv'] == 'heure d’Europe centrale' def test_hour_formatting(self): locale = 'en_US' t = time(0, 0, 0) assert dates.format_time(t, 'h a', locale=locale) == '12 AM' assert dates.format_time(t, 'H', locale=locale) == '0' assert dates.format_time(t, 'k', locale=locale) == '24' assert dates.format_time(t, 'K a', locale=locale) == '0 AM' t = time(12, 0, 0) assert dates.format_time(t, 'h a', locale=locale) == '12 PM' assert dates.format_time(t, 'H', locale=locale) == '12' assert dates.format_time(t, 'k', locale=locale) == '12' assert dates.format_time(t, 'K a', locale=locale) == '0 PM' class FormatDateTestCase: def test_with_time_fields_in_pattern(self): with pytest.raises(AttributeError): dates.format_date(date(2007, 4, 1), "yyyy-MM-dd HH:mm", locale='en_US') def test_with_time_fields_in_pattern_and_datetime_param(self): with pytest.raises(AttributeError): dates.format_date(datetime(2007, 4, 1, 15, 30), "yyyy-MM-dd HH:mm", locale='en_US') def test_with_day_of_year_in_pattern_and_datetime_param(self): # format_date should work on datetimes just as well (see #282) d = datetime(2007, 4, 1) assert dates.format_date(d, 'w', locale='en_US') == '14' class FormatDatetimeTestCase: def test_with_float(self, timezone_getter): UTC = timezone_getter('UTC') d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=UTC) epoch = float(calendar.timegm(d.timetuple())) formatted_string = dates.format_datetime(epoch, format='long', locale='en_US') assert formatted_string == 'April 1, 2012 at 3:30:29 PM UTC' def test_timezone_formats_los_angeles(self, timezone_getter): tz = timezone_getter('America/Los_Angeles') dt = _localize(tz, datetime(2016, 1, 13, 7, 8, 35)) assert dates.format_datetime(dt, 'z', locale='en') == 'PST' assert dates.format_datetime(dt, 'zz', locale='en') == 'PST' assert dates.format_datetime(dt, 'zzz', locale='en') == 'PST' assert dates.format_datetime(dt, 'zzzz', locale='en') == 'Pacific Standard Time' assert dates.format_datetime(dt, 'Z', locale='en') == '-0800' assert dates.format_datetime(dt, 'ZZ', locale='en') == '-0800' assert dates.format_datetime(dt, 'ZZZ', locale='en') == '-0800' assert dates.format_datetime(dt, 'ZZZZ', locale='en') == 'GMT-08:00' assert dates.format_datetime(dt, 'ZZZZZ', locale='en') == '-08:00' assert dates.format_datetime(dt, 'OOOO', locale='en') == 'GMT-08:00' assert dates.format_datetime(dt, 'VV', locale='en') == 'America/Los_Angeles' assert dates.format_datetime(dt, 'VVV', locale='en') == 'Los Angeles' assert dates.format_datetime(dt, 'X', locale='en') == '-08' assert dates.format_datetime(dt, 'XX', locale='en') == '-0800' assert dates.format_datetime(dt, 'XXX', locale='en') == '-08:00' assert dates.format_datetime(dt, 'XXXX', locale='en') == '-0800' assert dates.format_datetime(dt, 'XXXXX', locale='en') == '-08:00' assert dates.format_datetime(dt, 'x', locale='en') == '-08' assert dates.format_datetime(dt, 'xx', locale='en') == '-0800' assert dates.format_datetime(dt, 'xxx', locale='en') == '-08:00' assert dates.format_datetime(dt, 'xxxx', locale='en') == '-0800' assert dates.format_datetime(dt, 'xxxxx', locale='en') == '-08:00' def test_timezone_formats_utc(self, timezone_getter): tz = timezone_getter('UTC') dt = _localize(tz, datetime(2016, 1, 13, 7, 8, 35)) assert dates.format_datetime(dt, 'Z', locale='en') == '+0000' assert dates.format_datetime(dt, 'ZZ', locale='en') == '+0000' assert dates.format_datetime(dt, 'ZZZ', locale='en') == '+0000' assert dates.format_datetime(dt, 'ZZZZ', locale='en') == 'GMT+00:00' assert dates.format_datetime(dt, 'ZZZZZ', locale='en') == 'Z' assert dates.format_datetime(dt, 'OOOO', locale='en') == 'GMT+00:00' assert dates.format_datetime(dt, 'VV', locale='en') == 'Etc/UTC' assert dates.format_datetime(dt, 'VVV', locale='en') == 'UTC' assert dates.format_datetime(dt, 'X', locale='en') == 'Z' assert dates.format_datetime(dt, 'XX', locale='en') == 'Z' assert dates.format_datetime(dt, 'XXX', locale='en') == 'Z' assert dates.format_datetime(dt, 'XXXX', locale='en') == 'Z' assert dates.format_datetime(dt, 'XXXXX', locale='en') == 'Z' assert dates.format_datetime(dt, 'x', locale='en') == '+00' assert dates.format_datetime(dt, 'xx', locale='en') == '+0000' assert dates.format_datetime(dt, 'xxx', locale='en') == '+00:00' assert dates.format_datetime(dt, 'xxxx', locale='en') == '+0000' assert dates.format_datetime(dt, 'xxxxx', locale='en') == '+00:00' def test_timezone_formats_kolkata(self, timezone_getter): tz = timezone_getter('Asia/Kolkata') dt = _localize(tz, datetime(2016, 1, 13, 7, 8, 35)) assert dates.format_datetime(dt, 'zzzz', locale='en') == 'India Standard Time' assert dates.format_datetime(dt, 'ZZZZ', locale='en') == 'GMT+05:30' assert dates.format_datetime(dt, 'ZZZZZ', locale='en') == '+05:30' assert dates.format_datetime(dt, 'OOOO', locale='en') == 'GMT+05:30' assert dates.format_datetime(dt, 'VV', locale='en') == 'Asia/Calcutta' assert dates.format_datetime(dt, 'VVV', locale='en') == 'Kolkata' assert dates.format_datetime(dt, 'X', locale='en') == '+0530' assert dates.format_datetime(dt, 'XX', locale='en') == '+0530' assert dates.format_datetime(dt, 'XXX', locale='en') == '+05:30' assert dates.format_datetime(dt, 'XXXX', locale='en') == '+0530' assert dates.format_datetime(dt, 'XXXXX', locale='en') == '+05:30' assert dates.format_datetime(dt, 'x', locale='en') == '+0530' assert dates.format_datetime(dt, 'xx', locale='en') == '+0530' assert dates.format_datetime(dt, 'xxx', locale='en') == '+05:30' assert dates.format_datetime(dt, 'xxxx', locale='en') == '+0530' assert dates.format_datetime(dt, 'xxxxx', locale='en') == '+05:30' class FormatTimeTestCase: def test_with_naive_datetime_and_tzinfo(self, timezone_getter): assert dates.format_time( datetime(2007, 4, 1, 15, 30), 'long', tzinfo=timezone_getter('US/Eastern'), locale='en', ) == '11:30:00 AM EDT' def test_with_float(self, timezone_getter): tz = timezone_getter('UTC') d = _localize(tz, datetime(2012, 4, 1, 15, 30, 29)) epoch = float(calendar.timegm(d.timetuple())) assert dates.format_time(epoch, format='long', locale='en_US') == '3:30:29 PM UTC' def test_with_date_fields_in_pattern(self): with pytest.raises(AttributeError): dates.format_time(datetime(2007, 4, 1), 'yyyy-MM-dd HH:mm', locale='en') def test_with_date_fields_in_pattern_and_datetime_param(self): with pytest.raises(AttributeError): dates.format_time(datetime(2007, 4, 1, 15, 30), "yyyy-MM-dd HH:mm", locale='en_US') class FormatTimedeltaTestCase: def test_zero_seconds(self): td = timedelta(seconds=0) assert dates.format_timedelta(td, locale='en') == '0 seconds' assert dates.format_timedelta(td, locale='en', format='short') == '0 sec' assert dates.format_timedelta(td, granularity='hour', locale='en') == '0 hours' assert dates.format_timedelta(td, granularity='hour', locale='en', format='short') == '0 hr' def test_small_value_with_granularity(self): td = timedelta(seconds=42) assert dates.format_timedelta(td, granularity='hour', locale='en') == '1 hour' assert dates.format_timedelta(td, granularity='hour', locale='en', format='short') == '1 hr' def test_direction_adding(self): td = timedelta(hours=1) assert dates.format_timedelta(td, locale='en', add_direction=True) == 'in 1 hour' assert dates.format_timedelta(-td, locale='en', add_direction=True) == '1 hour ago' def test_format_narrow(self): assert dates.format_timedelta(timedelta(hours=1), locale='en', format='narrow') == '1h' assert dates.format_timedelta(timedelta(hours=-2), locale='en', format='narrow') == '2h' def test_format_invalid(self): for format in (None, '', 'bold italic'): with pytest.raises(TypeError): dates.format_timedelta(timedelta(hours=1), format=format) class TimeZoneAdjustTestCase: def _utc(self): class EvilFixedOffsetTimezone(FixedOffsetTimezone): def localize(self, dt, is_dst=False): raise NotImplementedError() UTC = EvilFixedOffsetTimezone(0, 'UTC') # This is important to trigger the actual bug (#257) assert hasattr(UTC, 'normalize') is False return UTC def test_can_format_time_with_custom_timezone(self): # regression test for #257 utc = self._utc() t = datetime(2007, 4, 1, 15, 30, tzinfo=utc) formatted_time = dates.format_time(t, 'long', tzinfo=utc, locale='en') assert formatted_time == '3:30:00 PM UTC' def test_get_period_names(): assert dates.get_period_names(locale='en_US')['am'] == 'AM' def test_get_day_names(): assert dates.get_day_names('wide', locale='en_US')[1] == 'Tuesday' assert dates.get_day_names('short', locale='en_US')[1] == 'Tu' assert dates.get_day_names('abbreviated', locale='es')[1] == 'mar' de = dates.get_day_names('narrow', context='stand-alone', locale='de_DE') assert de[1] == 'D' def test_get_month_names(): assert dates.get_month_names('wide', locale='en_US')[1] == 'January' assert dates.get_month_names('abbreviated', locale='es')[1] == 'ene' de = dates.get_month_names('narrow', context='stand-alone', locale='de_DE') assert de[1] == 'J' def test_get_quarter_names(): assert dates.get_quarter_names('wide', locale='en_US')[1] == '1st quarter' assert dates.get_quarter_names('abbreviated', locale='de_DE')[1] == 'Q1' assert dates.get_quarter_names('narrow', locale='de_DE')[1] == '1' def test_get_era_names(): assert dates.get_era_names('wide', locale='en_US')[1] == 'Anno Domini' assert dates.get_era_names('abbreviated', locale='de_DE')[1] == 'n. Chr.' def test_get_date_format(): us = dates.get_date_format(locale='en_US') assert us.pattern == 'MMM d, y' de = dates.get_date_format('full', locale='de_DE') assert de.pattern == 'EEEE, d. MMMM y' def test_get_datetime_format(): assert dates.get_datetime_format(locale='en_US') == '{1}, {0}' def test_get_time_format(): assert dates.get_time_format(locale='en_US').pattern == 'h:mm:ss\u202fa' assert (dates.get_time_format('full', locale='de_DE').pattern == 'HH:mm:ss zzzz') def test_get_timezone_gmt(timezone_getter): dt = datetime(2007, 4, 1, 15, 30) assert dates.get_timezone_gmt(dt, locale='en') == 'GMT+00:00' assert dates.get_timezone_gmt(dt, locale='en', return_z=True) == 'Z' assert dates.get_timezone_gmt(dt, locale='en', width='iso8601_short') == '+00' tz = timezone_getter('America/Los_Angeles') dt = _localize(tz, datetime(2007, 4, 1, 15, 30)) assert dates.get_timezone_gmt(dt, locale='en') == 'GMT-07:00' assert dates.get_timezone_gmt(dt, 'short', locale='en') == '-0700' assert dates.get_timezone_gmt(dt, locale='en', width='iso8601_short') == '-07' assert dates.get_timezone_gmt(dt, 'long', locale='fr_FR') == 'UTC-07:00' def test_get_timezone_location(timezone_getter): tz = timezone_getter('America/St_Johns') assert (dates.get_timezone_location(tz, locale='de_DE') == "Kanada (St. John\u2019s) (Ortszeit)") assert (dates.get_timezone_location(tz, locale='en') == 'Canada (St. John’s) Time') assert (dates.get_timezone_location(tz, locale='en', return_city=True) == 'St. John’s') tz = timezone_getter('America/Mexico_City') assert (dates.get_timezone_location(tz, locale='de_DE') == 'Mexiko (Mexiko-Stadt) (Ortszeit)') tz = timezone_getter('Europe/Berlin') assert (dates.get_timezone_location(tz, locale='de_DE') == 'Deutschland (Berlin) (Ortszeit)') @pytest.mark.parametrize( "tzname, params, expected", [ ("America/Los_Angeles", {"locale": "en_US"}, "Pacific Time"), ("America/Los_Angeles", {"width": "short", "locale": "en_US"}, "PT"), ("Europe/Berlin", {"locale": "de_DE"}, "Mitteleurop\xe4ische Zeit"), ("Europe/Berlin", {"locale": "pt_BR"}, "Hor\xe1rio da Europa Central"), ("America/St_Johns", {"locale": "de_DE"}, "Neufundland-Zeit"), ( "America/Los_Angeles", {"locale": "en", "width": "short", "zone_variant": "generic"}, "PT", ), ( "America/Los_Angeles", {"locale": "en", "width": "short", "zone_variant": "standard"}, "PST", ), ( "America/Los_Angeles", {"locale": "en", "width": "short", "zone_variant": "daylight"}, "PDT", ), ( "America/Los_Angeles", {"locale": "en", "width": "long", "zone_variant": "generic"}, "Pacific Time", ), ( "America/Los_Angeles", {"locale": "en", "width": "long", "zone_variant": "standard"}, "Pacific Standard Time", ), ( "America/Los_Angeles", {"locale": "en", "width": "long", "zone_variant": "daylight"}, "Pacific Daylight Time", ), ("Europe/Berlin", {"locale": "en_US"}, "Central European Time"), ], ) def test_get_timezone_name_tzinfo(timezone_getter, tzname, params, expected): tz = timezone_getter(tzname) assert dates.get_timezone_name(tz, **params) == expected @pytest.mark.parametrize("timezone_getter", ["pytz.timezone"], indirect=True) @pytest.mark.parametrize( "tzname, params, expected", [ ("America/Los_Angeles", {"locale": "en_US"}, "Pacific Standard Time"), ( "America/Los_Angeles", {"locale": "en_US", "return_zone": True}, "America/Los_Angeles", ), ("America/Los_Angeles", {"width": "short", "locale": "en_US"}, "PST"), ], ) def test_get_timezone_name_time_pytz(timezone_getter, tzname, params, expected): """pytz (by design) can't determine if the time is in DST or not, so it will always return Standard time""" dt = time(15, 30, tzinfo=timezone_getter(tzname)) assert dates.get_timezone_name(dt, **params) == expected def test_get_timezone_name_misc(timezone_getter): localnow = datetime.now(timezone_getter('UTC')).astimezone(dates.LOCALTZ) assert (dates.get_timezone_name(None, locale='en_US') == dates.get_timezone_name(localnow, locale='en_US')) assert (dates.get_timezone_name('Europe/Berlin', locale='en_US') == "Central European Time") assert (dates.get_timezone_name(1400000000, locale='en_US', width='short') == "Unknown Region (UTC) Time") assert (dates.get_timezone_name(time(16, 20), locale='en_US', width='short') == "UTC") def test_format_date(): d = date(2007, 4, 1) assert dates.format_date(d, locale='en_US') == 'Apr 1, 2007' assert (dates.format_date(d, format='full', locale='de_DE') == 'Sonntag, 1. April 2007') assert (dates.format_date(d, "EEE, MMM d, ''yy", locale='en') == "Sun, Apr 1, '07") def test_format_datetime(timezone_getter): dt = datetime(2007, 4, 1, 15, 30) assert (dates.format_datetime(dt, locale='en_US') == 'Apr 1, 2007, 3:30:00\u202fPM') full = dates.format_datetime( dt, 'full', tzinfo=timezone_getter('Europe/Paris'), locale='fr_FR', ) assert full == ( 'dimanche 1 avril 2007, 17:30:00 heure ' 'd’été d’Europe centrale' ) custom = dates.format_datetime( dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", tzinfo=timezone_getter('US/Eastern'), locale='en', ) assert custom == '2007.04.01 AD at 11:30:00 EDT' def test_format_time(timezone_getter): t = time(15, 30) assert dates.format_time(t, locale='en_US') == '3:30:00\u202fPM' assert dates.format_time(t, format='short', locale='de_DE') == '15:30' assert (dates.format_time(t, "hh 'o''clock' a", locale='en') == "03 o'clock PM") paris = timezone_getter('Europe/Paris') eastern = timezone_getter('US/Eastern') t = _localize(paris, datetime(2007, 4, 1, 15, 30)) fr = dates.format_time(t, format='full', tzinfo=paris, locale='fr_FR') assert fr == '15:30:00 heure d’été d’Europe centrale' custom = dates.format_time(t, "hh 'o''clock' a, zzzz", tzinfo=eastern, locale='en') assert custom == "09 o'clock AM, Eastern Daylight Time" with freezegun.freeze_time("2023-01-01"): t = time(15, 30) paris = dates.format_time(t, format='full', tzinfo=paris, locale='fr_FR') assert paris == '15:30:00 heure normale d’Europe centrale' us_east = dates.format_time(t, format='full', tzinfo=eastern, locale='en_US') assert us_east == '3:30:00\u202fPM Eastern Standard Time' def test_format_skeleton(timezone_getter): dt = datetime(2007, 4, 1, 15, 30) assert (dates.format_skeleton('yMEd', dt, locale='en_US') == 'Sun, 4/1/2007') assert (dates.format_skeleton('yMEd', dt, locale='th') == 'อา. 1/4/2007') assert (dates.format_skeleton('EHm', dt, locale='en') == 'Sun 15:30') assert (dates.format_skeleton('EHm', dt, tzinfo=timezone_getter('Asia/Bangkok'), locale='th') == 'อา. 22:30 น.') def test_format_timedelta(): assert (dates.format_timedelta(timedelta(weeks=12), locale='en_US') == '3 months') assert (dates.format_timedelta(timedelta(seconds=1), locale='es') == '1 segundo') assert (dates.format_timedelta(timedelta(hours=3), granularity='day', locale='en_US') == '1 day') assert (dates.format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US') == '1 day') assert (dates.format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US') == '23 hours') def test_parse_date(): assert dates.parse_date('4/1/04', locale='en_US') == date(2004, 4, 1) assert dates.parse_date('01.04.2004', locale='de_DE') == date(2004, 4, 1) assert dates.parse_date('2004-04-01', locale='sv_SE', format='short') == date(2004, 4, 1) @pytest.mark.parametrize('input, expected', [ # base case, fully qualified time ('15:30:00', time(15, 30)), # test digits ('15:30', time(15, 30)), ('3:30', time(3, 30)), ('00:30', time(0, 30)), # test am parsing ('03:30 am', time(3, 30)), ('3:30:21 am', time(3, 30, 21)), ('3:30 am', time(3, 30)), # test pm parsing ('03:30 pm', time(15, 30)), ('03:30 pM', time(15, 30)), ('03:30 Pm', time(15, 30)), ('03:30 PM', time(15, 30)), # test hour-only parsing ('4 pm', time(16, 0)), ]) def test_parse_time(input, expected): assert dates.parse_time(input, locale='en_US') == expected @pytest.mark.parametrize('case', ['', 'a', 'aaa']) @pytest.mark.parametrize('func', [dates.parse_date, dates.parse_time]) def test_parse_errors(case, func): with pytest.raises(dates.ParseError): func(case, locale='en_US') def test_datetime_format_get_week_number(): format = dates.DateTimeFormat(date(2006, 1, 8), Locale.parse('de_DE')) assert format.get_week_number(6) == 1 format = dates.DateTimeFormat(date(2006, 1, 8), Locale.parse('en_US')) assert format.get_week_number(6) == 2 def test_parse_pattern(): assert dates.parse_pattern("MMMMd").format == '%(MMMM)s%(d)s' assert (dates.parse_pattern("MMM d, yyyy").format == '%(MMM)s %(d)s, %(yyyy)s') assert (dates.parse_pattern("H:mm' Uhr 'z").format == '%(H)s:%(mm)s Uhr %(z)s') assert dates.parse_pattern("hh' o''clock'").format == "%(hh)s o'clock" def test_lithuanian_long_format(): assert ( dates.format_date(date(2015, 12, 10), locale='lt_LT', format='long') == '2015 m. gruodžio 10 d.' ) def test_zh_TW_format(): # Refs GitHub issue #378 assert dates.format_time(datetime(2016, 4, 8, 12, 34, 56), locale='zh_TW') == '中午12:34:56' def test_format_current_moment(): frozen_instant = datetime.now(UTC) with freezegun.freeze_time(time_to_freeze=frozen_instant): assert dates.format_datetime(locale="en_US") == dates.format_datetime(frozen_instant, locale="en_US") @pytest.mark.all_locales def test_no_inherit_metazone_marker_never_in_output(locale, timezone_getter): # See: https://github.com/python-babel/babel/issues/428 tz = timezone_getter('America/Los_Angeles') t = _localize(tz, datetime(2016, 1, 6, 7)) assert NO_INHERITANCE_MARKER not in dates.format_time(t, format='long', locale=locale) assert NO_INHERITANCE_MARKER not in dates.get_timezone_name(t, width='short', locale=locale) def test_no_inherit_metazone_formatting(timezone_getter): # See: https://github.com/python-babel/babel/issues/428 tz = timezone_getter('America/Los_Angeles') t = _localize(tz, datetime(2016, 1, 6, 7)) assert dates.format_time(t, format='long', locale='en_US') == "7:00:00\u202fAM PST" assert dates.format_time(t, format='long', locale='en_GB') == "07:00:00 Pacific Standard Time" assert dates.get_timezone_name(t, width='short', locale='en_US') == "PST" assert dates.get_timezone_name(t, width='short', locale='en_GB') == "Pacific Standard Time" def test_russian_week_numbering(): # See https://github.com/python-babel/babel/issues/485 v = date(2017, 1, 1) assert dates.format_date(v, format='YYYY-ww', locale='ru_RU') == '2016-52' # This would have returned 2017-01 prior to CLDR 32 assert dates.format_date(v, format='YYYY-ww', locale='de_DE') == '2016-52' def test_en_gb_first_weekday(): assert Locale.parse('en').first_week_day == 0 # Monday in general assert Locale.parse('en_US').first_week_day == 6 # Sunday in the US assert Locale.parse('en_GB').first_week_day == 0 # Monday in the UK def test_issue_798(): assert dates.format_timedelta(timedelta(), format='narrow', locale='es_US') == '0s' babel-2.14.0/tests/test_util.py0000644000175000017500000000625414536056757015752 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import __future__ import unittest from io import BytesIO import pytest from babel import util from babel.util import parse_future_flags class _FF: division = __future__.division.compiler_flag print_function = __future__.print_function.compiler_flag with_statement = __future__.with_statement.compiler_flag unicode_literals = __future__.unicode_literals.compiler_flag def test_distinct(): assert list(util.distinct([1, 2, 1, 3, 4, 4])) == [1, 2, 3, 4] assert list(util.distinct('foobar')) == ['f', 'o', 'b', 'a', 'r'] def test_pathmatch(): assert util.pathmatch('**.py', 'bar.py') assert util.pathmatch('**.py', 'foo/bar/baz.py') assert not util.pathmatch('**.py', 'templates/index.html') assert util.pathmatch('**/templates/*.html', 'templates/index.html') assert not util.pathmatch('**/templates/*.html', 'templates/foo/bar.html') assert util.pathmatch('^foo/**.py', 'foo/bar/baz/blah.py') assert not util.pathmatch('^foo/**.py', 'blah/foo/bar/baz.py') assert util.pathmatch('./foo/**.py', 'foo/bar/baz/blah.py') assert util.pathmatch('./blah.py', 'blah.py') assert not util.pathmatch('./foo/**.py', 'blah/foo/bar/baz.py') class FixedOffsetTimezoneTestCase(unittest.TestCase): def test_zone_negative_offset(self): assert util.FixedOffsetTimezone(-60).zone == 'Etc/GMT-60' def test_zone_zero_offset(self): assert util.FixedOffsetTimezone(0).zone == 'Etc/GMT+0' def test_zone_positive_offset(self): assert util.FixedOffsetTimezone(330).zone == 'Etc/GMT+330' def parse_encoding(s): return util.parse_encoding(BytesIO(s.encode('utf-8'))) def test_parse_encoding_defined(): assert parse_encoding('# coding: utf-8') == 'utf-8' def test_parse_encoding_undefined(): assert parse_encoding('') is None def test_parse_encoding_non_ascii(): assert parse_encoding('K\xf6ln') is None @pytest.mark.parametrize('source, result', [ (''' from __future__ import print_function, division, with_statement, unicode_literals ''', _FF.print_function | _FF.division | _FF.with_statement | _FF.unicode_literals), (''' from __future__ import print_function, division print('hello') ''', _FF.print_function | _FF.division), (''' from __future__ import print_function, division, unknown,,,,, print 'hello' ''', _FF.print_function | _FF.division), (''' from __future__ import ( print_function, division) ''', _FF.print_function | _FF.division), (''' from __future__ import \\ print_function, \\ division ''', _FF.print_function | _FF.division), ]) def test_parse_future(source, result): fp = BytesIO(source.encode('latin-1')) flags = parse_future_flags(fp) assert flags == result babel-2.14.0/tests/test_numbers.py0000644000175000017500000012336514536056757016453 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import decimal import unittest from datetime import date import pytest from babel import localedata, numbers from babel.numbers import ( UnknownCurrencyError, get_currency_precision, get_currency_unit_pattern, get_decimal_precision, is_currency, list_currencies, normalize_currency, validate_currency, ) class FormatDecimalTestCase(unittest.TestCase): def test_patterns(self): assert numbers.format_decimal(12345, '##0', locale='en_US') == '12345' assert numbers.format_decimal(6.5, '0.00', locale='sv') == '6,50' assert numbers.format_decimal((10.0 ** 20), '#.00', locale='en_US') == '100000000000000000000.00' # regression test for #183, fraction digits were not correctly cut # if the input was a float value and the value had more than 7 # significant digits assert numbers.format_decimal(12345678.051, '#,##0.00', locale='en_US') == '12,345,678.05' def test_subpatterns(self): assert numbers.format_decimal((- 12345), '#,##0.##;-#', locale='en_US') == '-12,345' assert numbers.format_decimal((- 12345), '#,##0.##;(#)', locale='en_US') == '(12,345)' def test_default_rounding(self): """ Testing Round-Half-Even (Banker's rounding) A '5' is rounded to the closest 'even' number """ assert numbers.format_decimal(5.5, '0', locale='sv') == '6' assert numbers.format_decimal(6.5, '0', locale='sv') == '6' assert numbers.format_decimal(6.5, '0', locale='sv') == '6' assert numbers.format_decimal(1.2325, locale='sv') == '1,232' assert numbers.format_decimal(1.2335, locale='sv') == '1,234' def test_significant_digits(self): """Test significant digits patterns""" assert numbers.format_decimal(123004, '@@', locale='en_US') == '120000' assert numbers.format_decimal(1.12, '@', locale='sv') == '1' assert numbers.format_decimal(1.1, '@@', locale='sv') == '1,1' assert numbers.format_decimal(1.1, '@@@@@##', locale='sv') == '1,1000' assert numbers.format_decimal(0.0001, '@@@', locale='sv') == '0,000100' assert numbers.format_decimal(0.0001234, '@@@', locale='sv') == '0,000123' assert numbers.format_decimal(0.0001234, '@@@#', locale='sv') == '0,0001234' assert numbers.format_decimal(0.0001234, '@@@#', locale='sv') == '0,0001234' assert numbers.format_decimal(0.12345, '@@@', locale='sv') == '0,123' assert numbers.format_decimal(3.14159, '@@##', locale='sv') == '3,142' assert numbers.format_decimal(1.23004, '@@##', locale='sv') == '1,23' assert numbers.format_decimal(1230.04, '@@,@@', locale='en_US') == '12,30' assert numbers.format_decimal(123.41, '@@##', locale='en_US') == '123.4' assert numbers.format_decimal(1, '@@', locale='en_US') == '1.0' assert numbers.format_decimal(0, '@', locale='en_US') == '0' assert numbers.format_decimal(0.1, '@', locale='en_US') == '0.1' assert numbers.format_decimal(0.1, '@#', locale='en_US') == '0.1' assert numbers.format_decimal(0.1, '@@', locale='en_US') == '0.10' def test_decimals(self): """Test significant digits patterns""" assert numbers.format_decimal(decimal.Decimal('1.2345'), '#.00', locale='en_US') == '1.23' assert numbers.format_decimal(decimal.Decimal('1.2345000'), '#.00', locale='en_US') == '1.23' assert numbers.format_decimal(decimal.Decimal('1.2345000'), '@@', locale='en_US') == '1.2' assert numbers.format_decimal(decimal.Decimal('12345678901234567890.12345'), '#.00', locale='en_US') == '12345678901234567890.12' def test_scientific_notation(self): assert numbers.format_scientific(0.1, '#E0', locale='en_US') == '1E-1' assert numbers.format_scientific(0.01, '#E0', locale='en_US') == '1E-2' assert numbers.format_scientific(10, '#E0', locale='en_US') == '1E1' assert numbers.format_scientific(1234, '0.###E0', locale='en_US') == '1.234E3' assert numbers.format_scientific(1234, '0.#E0', locale='en_US') == '1.2E3' # Exponent grouping assert numbers.format_scientific(12345, '##0.####E0', locale='en_US') == '1.2345E4' # Minimum number of int digits assert numbers.format_scientific(12345, '00.###E0', locale='en_US') == '12.345E3' assert numbers.format_scientific(-12345.6, '00.###E0', locale='en_US') == '-12.346E3' assert numbers.format_scientific(-0.01234, '00.###E0', locale='en_US') == '-12.34E-3' # Custom pattern suffix assert numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US') == '1.23E2 m/s' # Exponent patterns assert numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US') == '1.23E02 m/s' assert numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US') == '1.23E-02 m/s' assert numbers.format_scientific(decimal.Decimal('12345'), '#.##E+00 m/s', locale='en_US') == '1.23E+04 m/s' # 0 (see ticket #99) assert numbers.format_scientific(0, '#E0', locale='en_US') == '0E0' def test_formatting_of_very_small_decimals(self): # previously formatting very small decimals could lead to a type error # because the Decimal->string conversion was too simple (see #214) number = decimal.Decimal("7E-7") assert numbers.format_decimal(number, format="@@@", locale='en_US') == '0.000000700' def test_nan_and_infinity(self): assert numbers.format_decimal(decimal.Decimal('Infinity'), locale='en_US') == '∞' assert numbers.format_decimal(decimal.Decimal('-Infinity'), locale='en_US') == '-∞' assert numbers.format_decimal(decimal.Decimal('NaN'), locale='en_US') == 'NaN' assert numbers.format_compact_decimal(decimal.Decimal('Infinity'), locale='en_US', format_type="short") == '∞' assert numbers.format_compact_decimal(decimal.Decimal('-Infinity'), locale='en_US', format_type="short") == '-∞' assert numbers.format_compact_decimal(decimal.Decimal('NaN'), locale='en_US', format_type="short") == 'NaN' assert numbers.format_currency(decimal.Decimal('Infinity'), 'USD', locale='en_US') == '$∞' assert numbers.format_currency(decimal.Decimal('-Infinity'), 'USD', locale='en_US') == '-$∞' def test_group_separator(self): assert numbers.format_decimal(29567.12, locale='en_US', group_separator=False) == '29567.12' assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=False) == '29567,12' assert numbers.format_decimal(29567.12, locale='pt_BR', group_separator=False) == '29567,12' assert numbers.format_currency(1099.98, 'USD', locale='en_US', group_separator=False) == '$1099.98' assert numbers.format_currency(101299.98, 'EUR', locale='fr_CA', group_separator=False) == '101299,98\xa0€' assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=False, format_type='name') == '101299.98 euros' assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=False) == '25123412\xa0%' assert numbers.format_decimal(29567.12, locale='en_US', group_separator=True) == '29,567.12' assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=True) == '29\xa0567,12' assert numbers.format_decimal(29567.12, locale='pt_BR', group_separator=True) == '29.567,12' assert numbers.format_currency(1099.98, 'USD', locale='en_US', group_separator=True) == '$1,099.98' assert numbers.format_currency(101299.98, 'EUR', locale='fr_CA', group_separator=True) == '101\xa0299,98\xa0€' assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=True, format_type='name') == '101,299.98 euros' assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == '25\xa0123\xa0412\xa0%' def test_compact(self): assert numbers.format_compact_decimal(1, locale='en_US', format_type="short") == '1' assert numbers.format_compact_decimal(999, locale='en_US', format_type="short") == '999' assert numbers.format_compact_decimal(1000, locale='en_US', format_type="short") == '1K' assert numbers.format_compact_decimal(9000, locale='en_US', format_type="short") == '9K' assert numbers.format_compact_decimal(9123, locale='en_US', format_type="short", fraction_digits=2) == '9.12K' assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short") == '10K' assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short", fraction_digits=2) == '10K' assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="short") == '1M' assert numbers.format_compact_decimal(9000999, locale='en_US', format_type="short") == '9M' assert numbers.format_compact_decimal(9000900099, locale='en_US', format_type="short", fraction_digits=5) == '9.0009B' assert numbers.format_compact_decimal(1, locale='en_US', format_type="long") == '1' assert numbers.format_compact_decimal(999, locale='en_US', format_type="long") == '999' assert numbers.format_compact_decimal(1000, locale='en_US', format_type="long") == '1 thousand' assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long") == '9 thousand' assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long", fraction_digits=2) == '9 thousand' assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long") == '10 thousand' assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long", fraction_digits=2) == '10 thousand' assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="long") == '1 million' assert numbers.format_compact_decimal(9999999, locale='en_US', format_type="long") == '10 million' assert numbers.format_compact_decimal(9999999999, locale='en_US', format_type="long", fraction_digits=5) == '10 billion' assert numbers.format_compact_decimal(1, locale='ja_JP', format_type="short") == '1' assert numbers.format_compact_decimal(999, locale='ja_JP', format_type="short") == '999' assert numbers.format_compact_decimal(1000, locale='ja_JP', format_type="short") == '1000' assert numbers.format_compact_decimal(9123, locale='ja_JP', format_type="short") == '9123' assert numbers.format_compact_decimal(10000, locale='ja_JP', format_type="short") == '1万' assert numbers.format_compact_decimal(1234567, locale='ja_JP', format_type="long") == '123万' assert numbers.format_compact_decimal(-1, locale='en_US', format_type="short") == '-1' assert numbers.format_compact_decimal(-1234, locale='en_US', format_type="short", fraction_digits=2) == '-1.23K' assert numbers.format_compact_decimal(-123456789, format_type='short', locale='en_US') == '-123M' assert numbers.format_compact_decimal(-123456789, format_type='long', locale='en_US') == '-123 million' assert numbers.format_compact_decimal(2345678, locale='mk', format_type='long') == '2 милиони' assert numbers.format_compact_decimal(21000000, locale='mk', format_type='long') == '21 милион' assert numbers.format_compact_decimal(21345, locale="gv", format_type="short") == '21K' assert numbers.format_compact_decimal(1000, locale='it', format_type='long') == 'mille' assert numbers.format_compact_decimal(1234, locale='it', format_type='long') == '1 mila' assert numbers.format_compact_decimal(1000, locale='fr', format_type='long') == 'mille' assert numbers.format_compact_decimal(1234, locale='fr', format_type='long') == '1 millier' assert numbers.format_compact_decimal( 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default', ) == '12٫34\xa0ألف' assert numbers.format_compact_decimal( 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='latn', ) == '12.34\xa0ألف' class NumberParsingTestCase(unittest.TestCase): def test_can_parse_decimals(self): assert decimal.Decimal('1099.98') == numbers.parse_decimal('1,099.98', locale='en_US') assert decimal.Decimal('1099.98') == numbers.parse_decimal('1.099,98', locale='de') assert decimal.Decimal('1099.98') == numbers.parse_decimal('1٬099٫98', locale='ar', numbering_system="default") with pytest.raises(numbers.NumberFormatError): numbers.parse_decimal('2,109,998', locale='de') with pytest.raises(numbers.UnsupportedNumberingSystemError): numbers.parse_decimal('2,109,998', locale='de', numbering_system="unknown") def test_parse_decimal_strict_mode(self): # Numbers with a misplaced grouping symbol should be rejected with pytest.raises(numbers.NumberFormatError) as info: numbers.parse_decimal('11.11', locale='de', strict=True) assert info.value.suggestions == ['1.111', '11,11'] # Numbers with two misplaced grouping symbols should be rejected with pytest.raises(numbers.NumberFormatError) as info: numbers.parse_decimal('80.00.00', locale='de', strict=True) assert info.value.suggestions == ['800.000'] # Partially grouped numbers should be rejected with pytest.raises(numbers.NumberFormatError) as info: numbers.parse_decimal('2000,000', locale='en_US', strict=True) assert info.value.suggestions == ['2,000,000', '2,000'] # Numbers with duplicate grouping symbols should be rejected with pytest.raises(numbers.NumberFormatError) as info: numbers.parse_decimal('0,,000', locale='en_US', strict=True) assert info.value.suggestions == ['0'] # Return only suggestion for 0 on strict with pytest.raises(numbers.NumberFormatError) as info: numbers.parse_decimal('0.00', locale='de', strict=True) assert info.value.suggestions == ['0'] # Properly formatted numbers should be accepted assert str(numbers.parse_decimal('1.001', locale='de', strict=True)) == '1001' # Trailing zeroes should be accepted assert str(numbers.parse_decimal('3.00', locale='en_US', strict=True)) == '3.00' # Numbers with a grouping symbol and no trailing zeroes should be accepted assert str(numbers.parse_decimal('3,400.6', locale='en_US', strict=True)) == '3400.6' # Numbers with a grouping symbol and trailing zeroes (not all zeroes after decimal) should be accepted assert str(numbers.parse_decimal('3,400.60', locale='en_US', strict=True)) == '3400.60' # Numbers with a grouping symbol and trailing zeroes (all zeroes after decimal) should be accepted assert str(numbers.parse_decimal('3,400.00', locale='en_US', strict=True)) == '3400.00' assert str(numbers.parse_decimal('3,400.0000', locale='en_US', strict=True)) == '3400.0000' # Numbers with a grouping symbol and no decimal part should be accepted assert str(numbers.parse_decimal('3,800', locale='en_US', strict=True)) == '3800' # Numbers without any grouping symbol should be accepted assert str(numbers.parse_decimal('2000.1', locale='en_US', strict=True)) == '2000.1' # Numbers without any grouping symbol and no decimal should be accepted assert str(numbers.parse_decimal('2580', locale='en_US', strict=True)) == '2580' # High precision numbers should be accepted assert str(numbers.parse_decimal('5,000001', locale='fr', strict=True)) == '5.000001' def test_list_currencies(): assert isinstance(list_currencies(), set) assert list_currencies().issuperset(['BAD', 'BAM', 'KRO']) assert isinstance(list_currencies(locale='fr'), set) assert list_currencies('fr').issuperset(['BAD', 'BAM', 'KRO']) with pytest.raises(ValueError) as excinfo: list_currencies('yo!') assert excinfo.value.args[0] == "expected only letters, got 'yo!'" assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'} assert len(list_currencies()) == 305 def test_validate_currency(): validate_currency('EUR') with pytest.raises(UnknownCurrencyError) as excinfo: validate_currency('FUU') assert excinfo.value.args[0] == "Unknown currency 'FUU'." def test_is_currency(): assert is_currency('EUR') assert not is_currency('eUr') assert not is_currency('FUU') assert not is_currency('') assert not is_currency(None) assert not is_currency(' EUR ') assert not is_currency(' ') assert not is_currency([]) assert not is_currency(set()) def test_normalize_currency(): assert normalize_currency('EUR') == 'EUR' assert normalize_currency('eUr') == 'EUR' assert normalize_currency('FUU') is None assert normalize_currency('') is None assert normalize_currency(None) is None assert normalize_currency(' EUR ') is None assert normalize_currency(' ') is None assert normalize_currency([]) is None assert normalize_currency(set()) is None def test_get_currency_name(): assert numbers.get_currency_name('USD', locale='en_US') == 'US Dollar' assert numbers.get_currency_name('USD', count=2, locale='en_US') == 'US dollars' def test_get_currency_symbol(): assert numbers.get_currency_symbol('USD', 'en_US') == '$' def test_get_currency_precision(): assert get_currency_precision('EUR') == 2 assert get_currency_precision('JPY') == 0 def test_get_currency_unit_pattern(): assert get_currency_unit_pattern('USD', locale='en_US') == '{0} {1}' assert get_currency_unit_pattern('USD', locale='es_GT') == '{1} {0}' # 'ro' locale various pattern according to count assert get_currency_unit_pattern('USD', locale='ro', count=1) == '{0} {1}' assert get_currency_unit_pattern('USD', locale='ro', count=2) == '{0} {1}' assert get_currency_unit_pattern('USD', locale='ro', count=100) == '{0} de {1}' assert get_currency_unit_pattern('USD', locale='ro') == '{0} de {1}' def test_get_territory_currencies(): assert numbers.get_territory_currencies('AT', date(1995, 1, 1)) == ['ATS'] assert numbers.get_territory_currencies('AT', date(2011, 1, 1)) == ['EUR'] assert numbers.get_territory_currencies('US', date(2013, 1, 1)) == ['USD'] assert sorted(numbers.get_territory_currencies('US', date(2013, 1, 1), non_tender=True)) == ['USD', 'USN', 'USS'] assert numbers.get_territory_currencies('US', date(2013, 1, 1), include_details=True) == [{ 'currency': 'USD', 'from': date(1792, 1, 1), 'to': None, 'tender': True, }] assert numbers.get_territory_currencies('LS', date(2013, 1, 1)) == ['ZAR', 'LSL'] assert numbers.get_territory_currencies('QO', date(2013, 1, 1)) == [] # Croatia uses Euro starting in January 2023; this is in CLDR 42. # See https://github.com/python-babel/babel/issues/942 assert 'EUR' in numbers.get_territory_currencies('HR', date(2023, 1, 1)) def test_get_decimal_symbol(): assert numbers.get_decimal_symbol('en_US') == '.' assert numbers.get_decimal_symbol('en_US', numbering_system="default") == '.' assert numbers.get_decimal_symbol('en_US', numbering_system="latn") == '.' assert numbers.get_decimal_symbol('sv_SE') == ',' assert numbers.get_decimal_symbol('ar_EG') == '.' assert numbers.get_decimal_symbol('ar_EG', numbering_system="default") == '٫' assert numbers.get_decimal_symbol('ar_EG', numbering_system="latn") == '.' assert numbers.get_decimal_symbol('ar_EG', numbering_system="arab") == '٫' def test_get_plus_sign_symbol(): assert numbers.get_plus_sign_symbol('en_US') == '+' assert numbers.get_plus_sign_symbol('en_US', numbering_system="default") == '+' assert numbers.get_plus_sign_symbol('en_US', numbering_system="latn") == '+' assert numbers.get_plus_sign_symbol('ar_EG') == '\u200e+' assert numbers.get_plus_sign_symbol('ar_EG', numbering_system="default") == '\u061c+' assert numbers.get_plus_sign_symbol('ar_EG', numbering_system="arab") == '\u061c+' assert numbers.get_plus_sign_symbol('ar_EG', numbering_system="latn") == '\u200e+' def test_get_minus_sign_symbol(): assert numbers.get_minus_sign_symbol('en_US') == '-' assert numbers.get_minus_sign_symbol('en_US', numbering_system="default") == '-' assert numbers.get_minus_sign_symbol('en_US', numbering_system="latn") == '-' assert numbers.get_minus_sign_symbol('nl_NL') == '-' assert numbers.get_minus_sign_symbol('ar_EG') == '\u200e-' assert numbers.get_minus_sign_symbol('ar_EG', numbering_system="default") == '\u061c-' assert numbers.get_minus_sign_symbol('ar_EG', numbering_system="arab") == '\u061c-' assert numbers.get_minus_sign_symbol('ar_EG', numbering_system="latn") == '\u200e-' def test_get_exponential_symbol(): assert numbers.get_exponential_symbol('en_US') == 'E' assert numbers.get_exponential_symbol('en_US', numbering_system="latn") == 'E' assert numbers.get_exponential_symbol('en_US', numbering_system="default") == 'E' assert numbers.get_exponential_symbol('ja_JP') == 'E' assert numbers.get_exponential_symbol('ar_EG') == 'E' assert numbers.get_exponential_symbol('ar_EG', numbering_system="default") == 'اس' assert numbers.get_exponential_symbol('ar_EG', numbering_system="arab") == 'اس' assert numbers.get_exponential_symbol('ar_EG', numbering_system="latn") == 'E' def test_get_group_symbol(): assert numbers.get_group_symbol('en_US') == ',' assert numbers.get_group_symbol('en_US', numbering_system="latn") == ',' assert numbers.get_group_symbol('en_US', numbering_system="default") == ',' assert numbers.get_group_symbol('ar_EG') == ',' assert numbers.get_group_symbol('ar_EG', numbering_system="default") == '٬' assert numbers.get_group_symbol('ar_EG', numbering_system="arab") == '٬' assert numbers.get_group_symbol('ar_EG', numbering_system="latn") == ',' def test_get_infinity_symbol(): assert numbers.get_infinity_symbol('en_US') == '∞' assert numbers.get_infinity_symbol('ar_EG', numbering_system="latn") == '∞' assert numbers.get_infinity_symbol('ar_EG', numbering_system="default") == '∞' assert numbers.get_infinity_symbol('ar_EG', numbering_system="arab") == '∞' def test_decimal_precision(): assert get_decimal_precision(decimal.Decimal('0.110')) == 2 assert get_decimal_precision(decimal.Decimal('1.0')) == 0 assert get_decimal_precision(decimal.Decimal('10000')) == 0 def test_format_decimal(): assert numbers.format_decimal(1099, locale='en_US') == '1,099' assert numbers.format_decimal(1099, locale='de_DE') == '1.099' assert numbers.format_decimal(1.2345, locale='en_US') == '1.234' assert numbers.format_decimal(1.2346, locale='en_US') == '1.235' assert numbers.format_decimal(-1.2346, locale='en_US') == '-1.235' assert numbers.format_decimal(1.2345, locale='sv_SE') == '1,234' assert numbers.format_decimal(1.2345, locale='de') == '1,234' assert numbers.format_decimal(12345.5, locale='en_US') == '12,345.5' assert numbers.format_decimal(0001.2345000, locale='en_US') == '1.234' assert numbers.format_decimal(-0001.2346000, locale='en_US') == '-1.235' assert numbers.format_decimal(0000000.5, locale='en_US') == '0.5' assert numbers.format_decimal(000, locale='en_US') == '0' assert numbers.format_decimal(12345.5, locale='ar_EG') == '12,345.5' assert numbers.format_decimal(12345.5, locale='ar_EG', numbering_system="default") == '12٬345٫5' assert numbers.format_decimal(12345.5, locale='ar_EG', numbering_system="arab") == '12٬345٫5' with pytest.raises(numbers.UnsupportedNumberingSystemError): numbers.format_decimal(12345.5, locale='en_US', numbering_system="unknown") @pytest.mark.parametrize('input_value, expected_value', [ ('10000', '10,000'), ('1', '1'), ('1.0', '1'), ('1.1', '1.1'), ('1.11', '1.11'), ('1.110', '1.11'), ('1.001', '1.001'), ('1.00100', '1.001'), ('01.00100', '1.001'), ('101.00100', '101.001'), ('00000', '0'), ('0', '0'), ('0.0', '0'), ('0.1', '0.1'), ('0.11', '0.11'), ('0.110', '0.11'), ('0.001', '0.001'), ('0.00100', '0.001'), ('00.00100', '0.001'), ('000.00100', '0.001'), ]) def test_format_decimal_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_decimal( decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value def test_format_decimal_quantization(): # Test all locales. for locale_code in localedata.locale_identifiers(): assert numbers.format_decimal( '0.9999999999', locale=locale_code, decimal_quantization=False).endswith('9999999999') is True def test_format_currency(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US') == '$1,099.98') assert (numbers.format_currency(1099.98, 'USD', locale='en_US', numbering_system="default") == '$1,099.98') assert (numbers.format_currency(0, 'USD', locale='en_US') == '$0.00') assert (numbers.format_currency(1099.98, 'USD', locale='es_CO') == 'US$1.099,98') assert (numbers.format_currency(1099.98, 'EUR', locale='de_DE') == '1.099,98\xa0\u20ac') assert (numbers.format_currency(1099.98, 'USD', locale='ar_EG', numbering_system="default") == '\u200f1٬099٫98\xa0US$') assert (numbers.format_currency(1099.98, 'EUR', '\xa4\xa4 #,##0.00', locale='en_US') == 'EUR 1,099.98') assert (numbers.format_currency(1099.98, 'EUR', locale='nl_NL') != numbers.format_currency(-1099.98, 'EUR', locale='nl_NL')) assert (numbers.format_currency(1099.98, 'USD', format=None, locale='en_US') == '$1,099.98') assert (numbers.format_currency(1, 'USD', locale='es_AR') == 'US$1,00') # one assert (numbers.format_currency(1000000, 'USD', locale='es_AR') == 'US$1.000.000,00') # many assert (numbers.format_currency(0, 'USD', locale='es_AR') == 'US$0,00') # other def test_format_currency_format_type(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type="standard") == '$1,099.98') assert (numbers.format_currency(0, 'USD', locale='en_US', format_type="standard") == '$0.00') assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type="accounting") == '$1,099.98') assert (numbers.format_currency(0, 'USD', locale='en_US', format_type="accounting") == '$0.00') with pytest.raises(numbers.UnknownCurrencyFormatError) as excinfo: numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='unknown') assert excinfo.value.args[0] == "'unknown' is not a known currency format type" assert (numbers.format_currency(1099.98, 'JPY', locale='en_US') == '\xa51,100') assert (numbers.format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES') == '1.099,98') assert (numbers.format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False) == '\xa51,099.98') assert (numbers.format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES', currency_digits=False) == '1.099,98') def test_format_compact_currency(): assert numbers.format_compact_currency(1, 'USD', locale='en_US', format_type="short") == '$1' assert numbers.format_compact_currency(999, 'USD', locale='en_US', format_type="short") == '$999' assert numbers.format_compact_currency(123456789, 'USD', locale='en_US', format_type="short") == '$123M' assert numbers.format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2, format_type="short") == '$123.46M' assert numbers.format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2, format_type="short", numbering_system="default") == '$123.46M' assert numbers.format_compact_currency(-123456789, 'USD', locale='en_US', fraction_digits=2, format_type="short") == '-$123.46M' assert numbers.format_compact_currency(1, 'JPY', locale='ja_JP', format_type="short") == '¥1' assert numbers.format_compact_currency(1234, 'JPY', locale='ja_JP', format_type="short") == '¥1234' assert numbers.format_compact_currency(123456, 'JPY', locale='ja_JP', format_type="short") == '¥12万' assert numbers.format_compact_currency(123456, 'JPY', locale='ja_JP', format_type="short", fraction_digits=2) == '¥12.35万' assert numbers.format_compact_currency(123, 'EUR', locale='yav', format_type="short") == '€\xa0123' assert numbers.format_compact_currency(12345, 'EUR', locale='yav', format_type="short") == '€\xa012K' assert numbers.format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1) == '123,5\xa0Mio.\xa0€' assert numbers.format_compact_currency(123456789, 'USD', locale='ar_EG', fraction_digits=2, format_type="short", numbering_system="default") == '123٫46\xa0مليون\xa0US$' def test_format_compact_currency_invalid_format_type(): with pytest.raises(numbers.UnknownCurrencyFormatError): numbers.format_compact_currency(1099.98, 'USD', locale='en_US', format_type='unknown') @pytest.mark.parametrize('input_value, expected_value', [ ('10000', '$10,000.00'), ('1', '$1.00'), ('1.0', '$1.00'), ('1.1', '$1.10'), ('1.11', '$1.11'), ('1.110', '$1.11'), ('1.001', '$1.001'), ('1.00100', '$1.001'), ('01.00100', '$1.001'), ('101.00100', '$101.001'), ('00000', '$0.00'), ('0', '$0.00'), ('0.0', '$0.00'), ('0.1', '$0.10'), ('0.11', '$0.11'), ('0.110', '$0.11'), ('0.001', '$0.001'), ('0.00100', '$0.001'), ('00.00100', '$0.001'), ('000.00100', '$0.001'), ]) def test_format_currency_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_currency( decimal.Decimal(input_value), currency='USD', locale='en_US', decimal_quantization=False, ) == expected_value def test_format_currency_quantization(): # Test all locales. for locale_code in localedata.locale_identifiers(): assert numbers.format_currency( '0.9999999999', 'USD', locale=locale_code, decimal_quantization=False).find('9999999999') > -1 def test_format_currency_long_display_name(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='name') == '1,099.98 US dollars') assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='name', numbering_system="default") == '1,099.98 US dollars') assert (numbers.format_currency(1099.98, 'USD', locale='ar_EG', format_type='name', numbering_system="default") == '1٬099٫98 دولار أمريكي') assert (numbers.format_currency(1.00, 'USD', locale='en_US', format_type='name') == '1.00 US dollar') assert (numbers.format_currency(1.00, 'EUR', locale='en_US', format_type='name') == '1.00 euro') assert (numbers.format_currency(2, 'EUR', locale='en_US', format_type='name') == '2.00 euros') # This tests that '{1} {0}' unitPatterns are found: assert (numbers.format_currency(1, 'USD', locale='sw', format_type='name') == 'dola ya Marekani 1.00') # This tests unicode chars: assert (numbers.format_currency(1099.98, 'USD', locale='es_GT', format_type='name') == 'dólares estadounidenses 1,099.98') # Test for completely unknown currency, should fallback to currency code assert (numbers.format_currency(1099.98, 'XAB', locale='en_US', format_type='name') == '1,099.98 XAB') # Test for finding different unit patterns depending on count assert (numbers.format_currency(1, 'USD', locale='ro', format_type='name') == '1,00 dolar american') assert (numbers.format_currency(2, 'USD', locale='ro', format_type='name') == '2,00 dolari americani') assert (numbers.format_currency(100, 'USD', locale='ro', format_type='name') == '100,00 de dolari americani') def test_format_currency_long_display_name_all(): for locale_code in localedata.locale_identifiers(): assert numbers.format_currency( 1, 'USD', locale=locale_code, format_type='name').find('1') > -1 assert numbers.format_currency( '1', 'USD', locale=locale_code, format_type='name').find('1') > -1 def test_format_currency_long_display_name_custom_format(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='name', format='##0') == '1099.98 US dollars') assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='name', format='##0', currency_digits=False) == '1100 US dollars') def test_format_percent(): assert numbers.format_percent(0.34, locale='en_US') == '34%' assert numbers.format_percent(0.34, locale='en_US', numbering_system="default") == '34%' assert numbers.format_percent(0, locale='en_US') == '0%' assert numbers.format_percent(0.34, '##0%', locale='en_US') == '34%' assert numbers.format_percent(34, '##0', locale='en_US') == '34' assert numbers.format_percent(25.1234, locale='en_US') == '2,512%' assert (numbers.format_percent(25.1234, locale='sv_SE') == '2\xa0512\xa0%') assert (numbers.format_percent(25.1234, '#,##0\u2030', locale='en_US') == '25,123\u2030') assert numbers.format_percent(134.5, locale='ar_EG', numbering_system="default") == '13٬450%' @pytest.mark.parametrize('input_value, expected_value', [ ('100', '10,000%'), ('0.01', '1%'), ('0.010', '1%'), ('0.011', '1.1%'), ('0.0111', '1.11%'), ('0.01110', '1.11%'), ('0.01001', '1.001%'), ('0.0100100', '1.001%'), ('0.010100100', '1.01001%'), ('0.000000', '0%'), ('0', '0%'), ('0.00', '0%'), ('0.01', '1%'), ('0.011', '1.1%'), ('0.0110', '1.1%'), ('0.0001', '0.01%'), ('0.000100', '0.01%'), ('0.0000100', '0.001%'), ('0.00000100', '0.0001%'), ]) def test_format_percent_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_percent( decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value def test_format_percent_quantization(): # Test all locales. for locale_code in localedata.locale_identifiers(): assert numbers.format_percent( '0.9999999999', locale=locale_code, decimal_quantization=False).find('99999999') > -1 def test_format_scientific(): assert numbers.format_scientific(10000, locale='en_US') == '1E4' assert numbers.format_scientific(10000, locale='en_US', numbering_system="default") == '1E4' assert numbers.format_scientific(4234567, '#.#E0', locale='en_US') == '4.2E6' assert numbers.format_scientific(4234567, '0E0000', locale='en_US') == '4.234567E0006' assert numbers.format_scientific(4234567, '##0E00', locale='en_US') == '4.234567E06' assert numbers.format_scientific(4234567, '##00E00', locale='en_US') == '42.34567E05' assert numbers.format_scientific(4234567, '0,000E00', locale='en_US') == '4,234.567E03' assert numbers.format_scientific(4234567, '##0.#####E00', locale='en_US') == '4.23457E06' assert numbers.format_scientific(4234567, '##0.##E00', locale='en_US') == '4.23E06' assert numbers.format_scientific(42, '00000.000000E0000', locale='en_US') == '42000.000000E-0003' assert numbers.format_scientific(0.2, locale="ar_EG", numbering_system="default") == '2اس\u061c-1' def test_default_scientific_format(): """ Check the scientific format method auto-correct the rendering pattern in case of a missing fractional part. """ assert numbers.format_scientific(12345, locale='en_US') == '1.2345E4' assert numbers.format_scientific(12345.678, locale='en_US') == '1.2345678E4' assert numbers.format_scientific(12345, '#E0', locale='en_US') == '1.2345E4' assert numbers.format_scientific(12345.678, '#E0', locale='en_US') == '1.2345678E4' @pytest.mark.parametrize('input_value, expected_value', [ ('10000', '1E4'), ('1', '1E0'), ('1.0', '1E0'), ('1.1', '1.1E0'), ('1.11', '1.11E0'), ('1.110', '1.11E0'), ('1.001', '1.001E0'), ('1.00100', '1.001E0'), ('01.00100', '1.001E0'), ('101.00100', '1.01001E2'), ('00000', '0E0'), ('0', '0E0'), ('0.0', '0E0'), ('0.1', '1E-1'), ('0.11', '1.1E-1'), ('0.110', '1.1E-1'), ('0.001', '1E-3'), ('0.00100', '1E-3'), ('00.00100', '1E-3'), ('000.00100', '1E-3'), ]) def test_format_scientific_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_scientific( decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value def test_format_scientific_quantization(): # Test all locales. for locale_code in localedata.locale_identifiers(): assert numbers.format_scientific( '0.9999999999', locale=locale_code, decimal_quantization=False).find('999999999') > -1 def test_parse_number(): assert numbers.parse_number('1,099', locale='en_US') == 1099 assert numbers.parse_number('1.099', locale='de_DE') == 1099 assert numbers.parse_number('1٬099', locale='ar_EG', numbering_system="default") == 1099 with pytest.raises(numbers.NumberFormatError) as excinfo: numbers.parse_number('1.099,98', locale='de') assert excinfo.value.args[0] == "'1.099,98' is not a valid number" with pytest.raises(numbers.UnsupportedNumberingSystemError): numbers.parse_number('1.099,98', locale='en', numbering_system="unsupported") def test_parse_decimal(): assert (numbers.parse_decimal('1,099.98', locale='en_US') == decimal.Decimal('1099.98')) assert numbers.parse_decimal('1.099,98', locale='de') == decimal.Decimal('1099.98') with pytest.raises(numbers.NumberFormatError) as excinfo: numbers.parse_decimal('2,109,998', locale='de') assert excinfo.value.args[0] == "'2,109,998' is not a valid decimal number" def test_parse_grouping(): assert numbers.parse_grouping('##') == (1000, 1000) assert numbers.parse_grouping('#,###') == (3, 3) assert numbers.parse_grouping('#,####,###') == (3, 4) def test_parse_pattern(): # Original pattern is preserved np = numbers.parse_pattern('¤#,##0.00') assert np.pattern == '¤#,##0.00' np = numbers.parse_pattern('¤#,##0.00;(¤#,##0.00)') assert np.pattern == '¤#,##0.00;(¤#,##0.00)' # Given a NumberPattern object, we don't return a new instance. # However, we don't cache NumberPattern objects, so calling # parse_pattern with the same format string will create new # instances np1 = numbers.parse_pattern('¤ #,##0.00') np2 = numbers.parse_pattern('¤ #,##0.00') assert np1 is not np2 assert np1 is numbers.parse_pattern(np1) def test_parse_pattern_negative(): # No negative format specified np = numbers.parse_pattern('¤#,##0.00') assert np.prefix == ('¤', '-¤') assert np.suffix == ('', '') # Negative format is specified np = numbers.parse_pattern('¤#,##0.00;(¤#,##0.00)') assert np.prefix == ('¤', '(¤') assert np.suffix == ('', ')') # Negative sign is a suffix np = numbers.parse_pattern('¤ #,##0.00;¤ #,##0.00-') assert np.prefix == ('¤ ', '¤ ') assert np.suffix == ('', '-') def test_numberpattern_repr(): """repr() outputs the pattern string""" # This implementation looks a bit funny, but that's cause strings are # repr'd differently in Python 2 vs 3 and this test runs under both. format = '¤#,##0.00;(¤#,##0.00)' np = numbers.parse_pattern(format) assert repr(format) in repr(np) def test_parse_static_pattern(): assert numbers.parse_pattern('Kun') # in the So locale in CLDR 30 # TODO: static patterns might not be correctly `apply()`ed at present def test_parse_decimal_nbsp_heuristics(): # Re https://github.com/python-babel/babel/issues/637 – # for locales (of which there are many) that use U+00A0 as the group # separator in numbers, it's reasonable to assume that input strings # with plain spaces actually should have U+00A0s instead. # This heuristic is only applied when strict=False. n = decimal.Decimal("12345.123") assert numbers.parse_decimal("12 345.123", locale="fi") == n assert numbers.parse_decimal(numbers.format_decimal(n, locale="fi"), locale="fi") == n def test_very_small_decimal_no_quantization(): assert numbers.format_decimal(decimal.Decimal('1E-7'), locale='en', decimal_quantization=False) == '0.0000001' def test_single_quotes_in_pattern(): assert numbers.format_decimal(123, "'@0.#'00'@01'", locale='en') == '@0.#120@01' assert numbers.format_decimal(123, "'$'''0", locale='en') == "$'123" assert numbers.format_decimal(12, "'#'0 o''clock", locale='en') == "#12 o'clock" babel-2.14.0/tests/messages/0000755000175000017500000000000014536056757015164 5ustar nileshnileshbabel-2.14.0/tests/messages/test_frontend.py0000644000175000017500000015251514536056757020425 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import logging import os import shlex import shutil import sys import time import unittest from datetime import datetime, timedelta from io import BytesIO, StringIO from typing import List import pytest from freezegun import freeze_time from babel import __version__ as VERSION from babel.dates import format_datetime from babel.messages import Catalog, extract, frontend from babel.messages.frontend import ( BaseError, CommandLineInterface, ExtractMessages, OptionError, UpdateCatalog, ) from babel.messages.pofile import read_po, write_po from babel.util import LOCALTZ from tests.messages.consts import ( TEST_PROJECT_DISTRIBUTION_DATA, data_dir, i18n_dir, pot_file, project_dir, this_dir, ) def _po_file(locale): return os.path.join(i18n_dir, locale, 'LC_MESSAGES', 'messages.po') class Distribution: # subset of distutils.dist.Distribution def __init__(self, attrs: dict) -> None: self.attrs = attrs def get_name(self) -> str: return self.attrs['name'] def get_version(self) -> str: return self.attrs['version'] @property def packages(self) -> List[str]: return self.attrs['packages'] class CompileCatalogTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() os.chdir(data_dir) self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA) self.cmd = frontend.CompileCatalog(self.dist) self.cmd.initialize_options() def tearDown(self): os.chdir(self.olddir) def test_no_directory_or_output_file_specified(self): self.cmd.locale = 'en_US' self.cmd.input_file = 'dummy' with pytest.raises(OptionError): self.cmd.finalize_options() def test_no_directory_or_input_file_specified(self): self.cmd.locale = 'en_US' self.cmd.output_file = 'dummy' with pytest.raises(OptionError): self.cmd.finalize_options() class ExtractMessagesTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() os.chdir(data_dir) self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA) self.cmd = frontend.ExtractMessages(self.dist) self.cmd.initialize_options() def tearDown(self): if os.path.isfile(pot_file): os.unlink(pot_file) os.chdir(self.olddir) def assert_pot_file_exists(self): assert os.path.isfile(pot_file) def test_neither_default_nor_custom_keywords(self): self.cmd.output_file = 'dummy' self.cmd.no_default_keywords = True with pytest.raises(OptionError): self.cmd.finalize_options() def test_no_output_file_specified(self): with pytest.raises(OptionError): self.cmd.finalize_options() def test_both_sort_output_and_sort_by_file(self): self.cmd.output_file = 'dummy' self.cmd.sort_output = True self.cmd.sort_by_file = True with pytest.raises(OptionError): self.cmd.finalize_options() def test_invalid_file_or_dir_input_path(self): self.cmd.input_paths = 'nonexistent_path' self.cmd.output_file = 'dummy' with pytest.raises(OptionError): self.cmd.finalize_options() def test_input_paths_is_treated_as_list(self): self.cmd.input_paths = data_dir self.cmd.output_file = pot_file self.cmd.finalize_options() self.cmd.run() with open(pot_file) as f: catalog = read_po(f) msg = catalog.get('bar') assert len(msg.locations) == 1 assert ('file1.py' in msg.locations[0][0]) def test_input_paths_handle_spaces_after_comma(self): self.cmd.input_paths = f"{this_dir}, {data_dir}" self.cmd.output_file = pot_file self.cmd.finalize_options() assert self.cmd.input_paths == [this_dir, data_dir] def test_input_dirs_is_alias_for_input_paths(self): self.cmd.input_dirs = this_dir self.cmd.output_file = pot_file self.cmd.finalize_options() # Gets listified in `finalize_options`: assert self.cmd.input_paths == [self.cmd.input_dirs] def test_input_dirs_is_mutually_exclusive_with_input_paths(self): self.cmd.input_dirs = this_dir self.cmd.input_paths = this_dir self.cmd.output_file = pot_file with pytest.raises(OptionError): self.cmd.finalize_options() @freeze_time("1994-11-11") def test_extraction_with_default_mapping(self): self.cmd.copyright_holder = 'FooBar, Inc.' self.cmd.msgid_bugs_address = 'bugs.address@email.tld' self.cmd.output_file = 'project/i18n/temp.pot' self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' self.cmd.finalize_options() self.cmd.run() self.assert_pot_file_exists() date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Translations template for TestProject. # Copyright (C) {time.strftime('%Y')} FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , {time.strftime('%Y')}. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: {date}\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. TRANSLATOR: This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" #: project/ignored/this_wont_normally_be_here.py:11 msgid "FooBar" msgid_plural "FooBars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_extraction_with_mapping_file(self): self.cmd.copyright_holder = 'FooBar, Inc.' self.cmd.msgid_bugs_address = 'bugs.address@email.tld' self.cmd.mapping_file = 'mapping.cfg' self.cmd.output_file = 'project/i18n/temp.pot' self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' self.cmd.finalize_options() self.cmd.run() self.assert_pot_file_exists() date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Translations template for TestProject. # Copyright (C) {time.strftime('%Y')} FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , {time.strftime('%Y')}. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: {date}\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. TRANSLATOR: This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_extraction_with_mapping_dict(self): self.dist.message_extractors = { 'project': [ ('**/ignored/**.*', 'ignore', None), ('**.py', 'python', None), ], } self.cmd.copyright_holder = 'FooBar, Inc.' self.cmd.msgid_bugs_address = 'bugs.address@email.tld' self.cmd.output_file = 'project/i18n/temp.pot' self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:' self.cmd.finalize_options() self.cmd.run() self.assert_pot_file_exists() date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Translations template for TestProject. # Copyright (C) {time.strftime('%Y')} FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , {time.strftime('%Y')}. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: {date}\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. TRANSLATOR: This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content def test_extraction_add_location_file(self): self.dist.message_extractors = { 'project': [ ('**/ignored/**.*', 'ignore', None), ('**.py', 'python', None), ], } self.cmd.output_file = 'project/i18n/temp.pot' self.cmd.add_location = 'file' self.cmd.omit_header = True self.cmd.finalize_options() self.cmd.run() self.assert_pot_file_exists() expected_content = r"""#: project/file1.py msgid "bar" msgstr "" #: project/file2.py msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content class InitCatalogTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() os.chdir(data_dir) self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA) self.cmd = frontend.InitCatalog(self.dist) self.cmd.initialize_options() def tearDown(self): for dirname in ['en_US', 'ja_JP', 'lv_LV']: locale_dir = os.path.join(i18n_dir, dirname) if os.path.isdir(locale_dir): shutil.rmtree(locale_dir) os.chdir(self.olddir) def test_no_input_file(self): self.cmd.locale = 'en_US' self.cmd.output_file = 'dummy' with pytest.raises(OptionError): self.cmd.finalize_options() def test_no_locale(self): self.cmd.input_file = 'dummy' self.cmd.output_file = 'dummy' with pytest.raises(OptionError): self.cmd.finalize_options() @freeze_time("1994-11-11") def test_with_output_dir(self): self.cmd.input_file = 'project/i18n/messages.pot' self.cmd.locale = 'en_US' self.cmd.output_dir = 'project/i18n' self.cmd.finalize_options() self.cmd.run() po_file = _po_file('en_US') assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" "Language-Team: en_US \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_keeps_catalog_non_fuzzy(self): self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot' self.cmd.locale = 'en_US' self.cmd.output_dir = 'project/i18n' self.cmd.finalize_options() self.cmd.run() po_file = _po_file('en_US') assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" "Language-Team: en_US \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_correct_init_more_than_2_plurals(self): self.cmd.input_file = 'project/i18n/messages.pot' self.cmd.locale = 'lv_LV' self.cmd.output_dir = 'project/i18n' self.cmd.finalize_options() self.cmd.run() po_file = _po_file('lv_LV') assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Latvian (Latvia) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: lv_LV\n" "Language-Team: lv_LV \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :" " 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_correct_init_singular_plural_forms(self): self.cmd.input_file = 'project/i18n/messages.pot' self.cmd.locale = 'ja_JP' self.cmd.output_dir = 'project/i18n' self.cmd.finalize_options() self.cmd.run() po_file = _po_file('ja_JP') assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='ja_JP') expected_content = fr"""# Japanese (Japan) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: ja_JP\n" "Language-Team: ja_JP \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_supports_no_wrap(self): self.cmd.input_file = 'project/i18n/long_messages.pot' self.cmd.locale = 'en_US' self.cmd.output_dir = 'project/i18n' long_message = '"' + 'xxxxx ' * 15 + '"' with open('project/i18n/messages.pot', 'rb') as f: pot_contents = f.read().decode('latin-1') pot_with_very_long_line = pot_contents.replace('"bar"', long_message) with open(self.cmd.input_file, 'wb') as f: f.write(pot_with_very_long_line.encode('latin-1')) self.cmd.no_wrap = True self.cmd.finalize_options() self.cmd.run() po_file = _po_file('en_US') assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US') expected_content = fr"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" "Language-Team: en_US \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid {long_message} msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_supports_width(self): self.cmd.input_file = 'project/i18n/long_messages.pot' self.cmd.locale = 'en_US' self.cmd.output_dir = 'project/i18n' long_message = '"' + 'xxxxx ' * 15 + '"' with open('project/i18n/messages.pot', 'rb') as f: pot_contents = f.read().decode('latin-1') pot_with_very_long_line = pot_contents.replace('"bar"', long_message) with open(self.cmd.input_file, 'wb') as f: f.write(pot_with_very_long_line.encode('latin-1')) self.cmd.width = 120 self.cmd.finalize_options() self.cmd.run() po_file = _po_file('en_US') assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US') expected_content = fr"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" "Language-Team: en_US \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid {long_message} msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content class CommandLineInterfaceTestCase(unittest.TestCase): def setUp(self): data_dir = os.path.join(this_dir, 'data') self.orig_working_dir = os.getcwd() self.orig_argv = sys.argv self.orig_stdout = sys.stdout self.orig_stderr = sys.stderr sys.argv = ['pybabel'] sys.stdout = StringIO() sys.stderr = StringIO() os.chdir(data_dir) self._remove_log_handlers() self.cli = frontend.CommandLineInterface() def tearDown(self): os.chdir(self.orig_working_dir) sys.argv = self.orig_argv sys.stdout = self.orig_stdout sys.stderr = self.orig_stderr for dirname in ['lv_LV', 'ja_JP']: locale_dir = os.path.join(i18n_dir, dirname) if os.path.isdir(locale_dir): shutil.rmtree(locale_dir) self._remove_log_handlers() def _remove_log_handlers(self): # Logging handlers will be reused if possible (#227). This breaks the # implicit assumption that our newly created StringIO for sys.stderr # contains the console output. Removing the old handler ensures that a # new handler with our new StringIO instance will be used. log = logging.getLogger('babel') for handler in log.handlers: log.removeHandler(handler) def test_usage(self): try: self.cli.run(sys.argv) self.fail('Expected SystemExit') except SystemExit as e: assert e.code == 2 assert sys.stderr.getvalue().lower() == """\ usage: pybabel command [options] [args] pybabel: error: no valid command or option passed. try the -h/--help option for more information. """ def test_list_locales(self): """ Test the command with the --list-locales arg. """ result = self.cli.run(sys.argv + ['--list-locales']) assert not result output = sys.stdout.getvalue() assert 'fr_CH' in output assert 'French (Switzerland)' in output assert "\nb'" not in output # No bytes repr markers in output def _run_init_catalog(self): i18n_dir = os.path.join(data_dir, 'project', 'i18n') pot_path = os.path.join(data_dir, 'project', 'i18n', 'messages.pot') init_argv = sys.argv + ['init', '--locale', 'en_US', '-d', i18n_dir, '-i', pot_path] self.cli.run(init_argv) def test_no_duplicated_output_for_multiple_runs(self): self._run_init_catalog() first_output = sys.stderr.getvalue() self._run_init_catalog() second_output = sys.stderr.getvalue()[len(first_output):] # in case the log message is not duplicated we should get the same # output as before assert first_output == second_output def test_frontend_can_log_to_predefined_handler(self): custom_stream = StringIO() log = logging.getLogger('babel') log.addHandler(logging.StreamHandler(custom_stream)) self._run_init_catalog() assert id(sys.stderr) != id(custom_stream) assert not sys.stderr.getvalue() assert custom_stream.getvalue() def test_help(self): try: self.cli.run(sys.argv + ['--help']) self.fail('Expected SystemExit') except SystemExit as e: assert not e.code content = sys.stdout.getvalue().lower() assert 'options:' in content assert all(command in content for command in ('init', 'update', 'compile', 'extract')) def assert_pot_file_exists(self): assert os.path.isfile(pot_file) @freeze_time("1994-11-11") def test_extract_with_default_mapping(self): self.cli.run(sys.argv + ['extract', '--copyright-holder', 'FooBar, Inc.', '--project', 'TestProject', '--version', '0.1', '--msgid-bugs-address', 'bugs.address@email.tld', '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', '-o', pot_file, 'project']) self.assert_pot_file_exists() date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Translations template for TestProject. # Copyright (C) {time.strftime('%Y')} FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , {time.strftime('%Y')}. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: {date}\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. TRANSLATOR: This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" #: project/ignored/this_wont_normally_be_here.py:11 msgid "FooBar" msgid_plural "FooBars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_extract_with_mapping_file(self): self.cli.run(sys.argv + ['extract', '--copyright-holder', 'FooBar, Inc.', '--project', 'TestProject', '--version', '0.1', '--msgid-bugs-address', 'bugs.address@email.tld', '--mapping', os.path.join(data_dir, 'mapping.cfg'), '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', '-o', pot_file, 'project']) self.assert_pot_file_exists() date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Translations template for TestProject. # Copyright (C) {time.strftime('%Y')} FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , {time.strftime('%Y')}. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: {date}\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. TRANSLATOR: This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_extract_with_exact_file(self): """Tests that we can call extract with a particular file and only strings from that file get extracted. (Note the absence of strings from file1.py) """ file_to_extract = os.path.join(data_dir, 'project', 'file2.py') self.cli.run(sys.argv + ['extract', '--copyright-holder', 'FooBar, Inc.', '--project', 'TestProject', '--version', '0.1', '--msgid-bugs-address', 'bugs.address@email.tld', '--mapping', os.path.join(data_dir, 'mapping.cfg'), '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', '-o', pot_file, file_to_extract]) self.assert_pot_file_exists() date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Translations template for TestProject. # Copyright (C) {time.strftime('%Y')} FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , {time.strftime('%Y')}. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: {date}\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(pot_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_init_with_output_dir(self): po_file = _po_file('en_US') self.cli.run(sys.argv + ['init', '--locale', 'en_US', '-d', os.path.join(i18n_dir), '-i', os.path.join(i18n_dir, 'messages.pot')]) assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" "Language-Team: en_US \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_init_singular_plural_forms(self): po_file = _po_file('ja_JP') self.cli.run(sys.argv + ['init', '--locale', 'ja_JP', '-d', os.path.join(i18n_dir), '-i', os.path.join(i18n_dir, 'messages.pot')]) assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Japanese (Japan) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: ja_JP\n" "Language-Team: ja_JP \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content @freeze_time("1994-11-11") def test_init_more_than_2_plural_forms(self): po_file = _po_file('lv_LV') self.cli.run(sys.argv + ['init', '--locale', 'lv_LV', '-d', i18n_dir, '-i', os.path.join(i18n_dir, 'messages.pot')]) assert os.path.isfile(po_file) date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en') expected_content = fr"""# Latvian (Latvia) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language: lv_LV\n" "Language-Team: lv_LV \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :" " 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" """ with open(po_file) as f: actual_content = f.read() assert expected_content == actual_content def test_compile_catalog(self): po_file = _po_file('de_DE') mo_file = po_file.replace('.po', '.mo') self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', '-d', i18n_dir]) assert not os.path.isfile(mo_file), f'Expected no file at {mo_file!r}' assert sys.stderr.getvalue() == f'catalog {po_file} is marked as fuzzy, skipping\n' def test_compile_fuzzy_catalog(self): po_file = _po_file('de_DE') mo_file = po_file.replace('.po', '.mo') try: self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', '--use-fuzzy', '-d', i18n_dir]) assert os.path.isfile(mo_file) assert sys.stderr.getvalue() == f'compiling catalog {po_file} to {mo_file}\n' finally: if os.path.isfile(mo_file): os.unlink(mo_file) def test_compile_catalog_with_more_than_2_plural_forms(self): po_file = _po_file('ru_RU') mo_file = po_file.replace('.po', '.mo') try: self.cli.run(sys.argv + ['compile', '--locale', 'ru_RU', '--use-fuzzy', '-d', i18n_dir]) assert os.path.isfile(mo_file) assert sys.stderr.getvalue() == f'compiling catalog {po_file} to {mo_file}\n' finally: if os.path.isfile(mo_file): os.unlink(mo_file) def test_compile_catalog_multidomain(self): po_foo = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'foo.po') po_bar = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'bar.po') mo_foo = po_foo.replace('.po', '.mo') mo_bar = po_bar.replace('.po', '.mo') try: self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', '--domain', 'foo bar', '--use-fuzzy', '-d', i18n_dir]) for mo_file in [mo_foo, mo_bar]: assert os.path.isfile(mo_file) assert sys.stderr.getvalue() == ( f'compiling catalog {po_foo} to {mo_foo}\n' f'compiling catalog {po_bar} to {mo_bar}\n' ) finally: for mo_file in [mo_foo, mo_bar]: if os.path.isfile(mo_file): os.unlink(mo_file) def test_update(self): template = Catalog() template.add("1") template.add("2") template.add("3") tmpl_file = os.path.join(i18n_dir, 'temp-template.pot') with open(tmpl_file, "wb") as outfp: write_po(outfp, template) po_file = os.path.join(i18n_dir, 'temp1.po') self.cli.run(sys.argv + ['init', '-l', 'fi', '-o', po_file, '-i', tmpl_file, ]) with open(po_file) as infp: catalog = read_po(infp) assert len(catalog) == 3 # Add another entry to the template template.add("4") with open(tmpl_file, "wb") as outfp: write_po(outfp, template) self.cli.run(sys.argv + ['update', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) with open(po_file) as infp: catalog = read_po(infp) assert len(catalog) == 4 # Catalog was updated def test_update_pot_creation_date(self): template = Catalog() template.add("1") template.add("2") template.add("3") tmpl_file = os.path.join(i18n_dir, 'temp-template.pot') with open(tmpl_file, "wb") as outfp: write_po(outfp, template) po_file = os.path.join(i18n_dir, 'temp1.po') self.cli.run(sys.argv + ['init', '-l', 'fi', '-o', po_file, '-i', tmpl_file, ]) with open(po_file) as infp: catalog = read_po(infp) assert len(catalog) == 3 original_catalog_creation_date = catalog.creation_date # Update the template creation date template.creation_date -= timedelta(minutes=3) with open(tmpl_file, "wb") as outfp: write_po(outfp, template) self.cli.run(sys.argv + ['update', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) with open(po_file) as infp: catalog = read_po(infp) # We didn't ignore the creation date, so expect a diff assert catalog.creation_date != original_catalog_creation_date # Reset the "original" original_catalog_creation_date = catalog.creation_date # Update the template creation date again # This time, pass the ignore flag and expect the times are different template.creation_date -= timedelta(minutes=5) with open(tmpl_file, "wb") as outfp: write_po(outfp, template) self.cli.run(sys.argv + ['update', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file, '--ignore-pot-creation-date']) with open(po_file) as infp: catalog = read_po(infp) # We ignored creation date, so it should not have changed assert catalog.creation_date == original_catalog_creation_date def test_check(self): template = Catalog() template.add("1") template.add("2") template.add("3") tmpl_file = os.path.join(i18n_dir, 'temp-template.pot') with open(tmpl_file, "wb") as outfp: write_po(outfp, template) po_file = os.path.join(i18n_dir, 'temp1.po') self.cli.run(sys.argv + ['init', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file, ]) # Update the catalog file self.cli.run(sys.argv + ['update', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Run a check without introducing any changes to the template self.cli.run(sys.argv + ['update', '--check', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Add a new entry and expect the check to fail template.add("4") with open(tmpl_file, "wb") as outfp: write_po(outfp, template) with pytest.raises(BaseError): self.cli.run(sys.argv + ['update', '--check', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Write the latest changes to the po-file self.cli.run(sys.argv + ['update', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Update an entry and expect the check to fail template.add("4", locations=[("foo.py", 1)]) with open(tmpl_file, "wb") as outfp: write_po(outfp, template) with pytest.raises(BaseError): self.cli.run(sys.argv + ['update', '--check', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) def test_check_pot_creation_date(self): template = Catalog() template.add("1") template.add("2") template.add("3") tmpl_file = os.path.join(i18n_dir, 'temp-template.pot') with open(tmpl_file, "wb") as outfp: write_po(outfp, template) po_file = os.path.join(i18n_dir, 'temp1.po') self.cli.run(sys.argv + ['init', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file, ]) # Update the catalog file self.cli.run(sys.argv + ['update', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Run a check without introducing any changes to the template self.cli.run(sys.argv + ['update', '--check', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Run a check after changing the template creation date template.creation_date = datetime.now() - timedelta(minutes=5) with open(tmpl_file, "wb") as outfp: write_po(outfp, template) # Should fail without --ignore-pot-creation-date flag with pytest.raises(BaseError): self.cli.run(sys.argv + ['update', '--check', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) # Should pass with --ignore-pot-creation-date flag self.cli.run(sys.argv + ['update', '--check', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file, '--ignore-pot-creation-date']) def test_update_init_missing(self): template = Catalog() template.add("1") template.add("2") template.add("3") tmpl_file = os.path.join(i18n_dir, 'temp2-template.pot') with open(tmpl_file, "wb") as outfp: write_po(outfp, template) po_file = os.path.join(i18n_dir, 'temp2.po') self.cli.run(sys.argv + ['update', '--init-missing', '-l', 'fi', '-o', po_file, '-i', tmpl_file]) with open(po_file) as infp: catalog = read_po(infp) assert len(catalog) == 3 # Add another entry to the template template.add("4") with open(tmpl_file, "wb") as outfp: write_po(outfp, template) self.cli.run(sys.argv + ['update', '--init-missing', '-l', 'fi_FI', '-o', po_file, '-i', tmpl_file]) with open(po_file) as infp: catalog = read_po(infp) assert len(catalog) == 4 # Catalog was updated def test_parse_mapping(): buf = StringIO( '[extractors]\n' 'custom = mypackage.module:myfunc\n' '\n' '# Python source files\n' '[python: **.py]\n' '\n' '# Genshi templates\n' '[genshi: **/templates/**.html]\n' 'include_attrs =\n' '[genshi: **/templates/**.txt]\n' 'template_class = genshi.template:TextTemplate\n' 'encoding = latin-1\n' '\n' '# Some custom extractor\n' '[custom: **/custom/*.*]\n') method_map, options_map = frontend.parse_mapping(buf) assert len(method_map) == 4 assert method_map[0] == ('**.py', 'python') assert options_map['**.py'] == {} assert method_map[1] == ('**/templates/**.html', 'genshi') assert options_map['**/templates/**.html']['include_attrs'] == '' assert method_map[2] == ('**/templates/**.txt', 'genshi') assert (options_map['**/templates/**.txt']['template_class'] == 'genshi.template:TextTemplate') assert options_map['**/templates/**.txt']['encoding'] == 'latin-1' assert method_map[3] == ('**/custom/*.*', 'mypackage.module:myfunc') assert options_map['**/custom/*.*'] == {} def test_parse_keywords(): kw = frontend.parse_keywords(['_', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2']) assert kw == { '_': None, 'dgettext': (2,), 'dngettext': (2, 3), 'pgettext': ((1, 'c'), 2), } def test_parse_keywords_with_t(): kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t']) assert kw == { '_': { None: (1,), 2: (2,), 3: ((2, 'c'), 3), }, } def test_extract_messages_with_t(): content = rb""" _("1 arg, arg 1") _("2 args, arg 1", "2 args, arg 2") _("3 args, arg 1", "3 args, arg 2", "3 args, arg 3") _("4 args, arg 1", "4 args, arg 2", "4 args, arg 3", "4 args, arg 4") """ kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t']) result = list(extract.extract("python", BytesIO(content), kw)) expected = [(2, '1 arg, arg 1', [], None), (3, '2 args, arg 1', [], None), (3, '2 args, arg 2', [], None), (4, '3 args, arg 1', [], None), (4, '3 args, arg 3', [], '3 args, arg 2'), (5, '4 args, arg 1', [], None)] assert result == expected def configure_cli_command(cmdline): """ Helper to configure a command class, but not run it just yet. :param cmdline: The command line (sans the executable name) :return: Command instance """ args = shlex.split(cmdline) cli = CommandLineInterface() cmdinst = cli._configure_command(cmdname=args[0], argv=args[1:]) return cmdinst @pytest.mark.parametrize("split", (False, True)) @pytest.mark.parametrize("arg_name", ("-k", "--keyword", "--keywords")) def test_extract_keyword_args_384(split, arg_name): # This is a regression test for https://github.com/python-babel/babel/issues/384 # and it also tests that the rest of the forgotten aliases/shorthands implied by # https://github.com/python-babel/babel/issues/390 are re-remembered (or rather # that the mechanism for remembering them again works). kwarg_specs = [ "gettext_noop", "gettext_lazy", "ngettext_lazy:1,2", "ugettext_noop", "ugettext_lazy", "ungettext_lazy:1,2", "pgettext_lazy:1c,2", "npgettext_lazy:1c,2,3", ] if split: # Generate a command line with multiple -ks kwarg_text = " ".join(f"{arg_name} {kwarg_spec}" for kwarg_spec in kwarg_specs) else: # Generate a single space-separated -k specs = ' '.join(kwarg_specs) kwarg_text = f'{arg_name} "{specs}"' # (Both of those invocation styles should be equivalent, so there is no parametrization from here on out) cmdinst = configure_cli_command( f"extract -F babel-django.cfg --add-comments Translators: -o django232.pot {kwarg_text} .", ) assert isinstance(cmdinst, ExtractMessages) assert set(cmdinst.keywords.keys()) == {'_', 'dgettext', 'dngettext', 'gettext', 'gettext_lazy', 'gettext_noop', 'N_', 'ngettext', 'ngettext_lazy', 'npgettext', 'npgettext_lazy', 'pgettext', 'pgettext_lazy', 'ugettext', 'ugettext_lazy', 'ugettext_noop', 'ungettext', 'ungettext_lazy'} def test_update_catalog_boolean_args(): cmdinst = configure_cli_command( "update --init-missing --no-wrap -N --ignore-obsolete --previous -i foo -o foo -l en") assert isinstance(cmdinst, UpdateCatalog) assert cmdinst.init_missing is True assert cmdinst.no_wrap is True assert cmdinst.no_fuzzy_matching is True assert cmdinst.ignore_obsolete is True assert cmdinst.previous is False # Mutually exclusive with no_fuzzy_matching def test_extract_cli_knows_dash_s(): # This is a regression test for https://github.com/python-babel/babel/issues/390 cmdinst = configure_cli_command("extract -s -o foo babel") assert isinstance(cmdinst, ExtractMessages) assert cmdinst.strip_comments def test_extract_cli_knows_dash_dash_last_dash_translator(): cmdinst = configure_cli_command('extract --last-translator "FULL NAME EMAIL@ADDRESS" -o foo babel') assert isinstance(cmdinst, ExtractMessages) assert cmdinst.last_translator == "FULL NAME EMAIL@ADDRESS" def test_extract_add_location(): cmdinst = configure_cli_command("extract -o foo babel --add-location full") assert isinstance(cmdinst, ExtractMessages) assert cmdinst.add_location == 'full' assert not cmdinst.no_location assert cmdinst.include_lineno cmdinst = configure_cli_command("extract -o foo babel --add-location file") assert isinstance(cmdinst, ExtractMessages) assert cmdinst.add_location == 'file' assert not cmdinst.no_location assert not cmdinst.include_lineno cmdinst = configure_cli_command("extract -o foo babel --add-location never") assert isinstance(cmdinst, ExtractMessages) assert cmdinst.add_location == 'never' assert cmdinst.no_location def test_extract_error_code(monkeypatch, capsys): monkeypatch.chdir(project_dir) cmdinst = configure_cli_command("compile --domain=messages --directory i18n --locale fi_BUGGY") assert cmdinst.run() == 1 out, err = capsys.readouterr() if err: # replace hack below for py2/py3 compatibility assert "unknown named placeholder 'merkki'" in err.replace("u'", "'") @pytest.mark.parametrize("with_underscore_ignore", (False, True)) def test_extract_ignore_dirs(monkeypatch, capsys, tmp_path, with_underscore_ignore): pot_file = tmp_path / 'temp.pot' monkeypatch.chdir(project_dir) cmd = f"extract . -o '{pot_file}' --ignore-dirs '*ignored*' " if with_underscore_ignore: # This also tests that multiple arguments are supported. cmd += "--ignore-dirs '_*'" cmdinst = configure_cli_command(cmd) assert isinstance(cmdinst, ExtractMessages) assert cmdinst.directory_filter cmdinst.run() pot_content = pot_file.read_text() # The `ignored` directory is now actually ignored: assert 'this_wont_normally_be_here' not in pot_content # Since we manually set a filter, the otherwise `_hidden` directory is walked into, # unless we opt in to ignore it again assert ('ssshhh....' in pot_content) != with_underscore_ignore assert ('_hidden_by_default' in pot_content) != with_underscore_ignore def test_extract_header_comment(monkeypatch, tmp_path): pot_file = tmp_path / 'temp.pot' monkeypatch.chdir(project_dir) cmdinst = configure_cli_command(f"extract . -o '{pot_file}' --header-comment 'Boing' ") cmdinst.run() pot_content = pot_file.read_text() assert 'Boing' in pot_content babel-2.14.0/tests/messages/test_plurals.py0000644000175000017500000000501614536056757020261 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import pytest from babel import Locale from babel.messages import plurals @pytest.mark.parametrize(('locale', 'num_plurals', 'plural_expr'), [ (Locale('en'), 2, '(n != 1)'), (Locale('en', 'US'), 2, '(n != 1)'), (Locale('zh'), 1, '0'), (Locale('zh', script='Hans'), 1, '0'), (Locale('zh', script='Hant'), 1, '0'), (Locale('zh', 'CN', 'Hans'), 1, '0'), (Locale('zh', 'TW', 'Hant'), 1, '0'), ]) def test_get_plural_selection(locale, num_plurals, plural_expr): assert plurals.get_plural(locale) == (num_plurals, plural_expr) def test_get_plural_accepts_strings(): assert plurals.get_plural(locale='ga') == (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)') def test_get_plural_falls_back_to_default(): assert plurals.get_plural('ii') == (2, '(n != 1)') def test_get_plural(): # See https://localization-guide.readthedocs.io/en/latest/l10n/pluralforms.html for more details. assert plurals.get_plural(locale='en') == (2, '(n != 1)') assert plurals.get_plural(locale='ga') == (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)') plural_ja = plurals.get_plural("ja") assert str(plural_ja) == 'nplurals=1; plural=0;' assert plural_ja.num_plurals == 1 assert plural_ja.plural_expr == '0' assert plural_ja.plural_forms == 'nplurals=1; plural=0;' plural_en_US = plurals.get_plural('en_US') assert str(plural_en_US) == 'nplurals=2; plural=(n != 1);' assert plural_en_US.num_plurals == 2 assert plural_en_US.plural_expr == '(n != 1)' plural_fr_FR = plurals.get_plural('fr_FR') assert str(plural_fr_FR) == 'nplurals=2; plural=(n > 1);' assert plural_fr_FR.num_plurals == 2 assert plural_fr_FR.plural_expr == '(n > 1)' plural_pl_PL = plurals.get_plural('pl_PL') assert str(plural_pl_PL) == 'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);' assert plural_pl_PL.num_plurals == 3 assert plural_pl_PL.plural_expr == '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)' babel-2.14.0/tests/messages/test_jslexer.py0000644000175000017500000000740114536056757020253 0ustar nileshnileshfrom babel.messages import jslexer def test_unquote(): assert jslexer.unquote_string('""') == '' assert jslexer.unquote_string(r'"h\u00ebllo"') == "hëllo" assert jslexer.unquote_string(r'"h\xebllo"') == "hëllo" assert jslexer.unquote_string(r'"\xebb"') == "ëb" def test_dollar_in_identifier(): assert list(jslexer.tokenize('dollar$dollar')) == [('name', 'dollar$dollar', 1)] def test_dotted_name(): assert list(jslexer.tokenize("foo.bar(quux)", dotted=True)) == [ ('name', 'foo.bar', 1), ('operator', '(', 1), ('name', 'quux', 1), ('operator', ')', 1), ] def test_dotted_name_end(): assert list(jslexer.tokenize("foo.bar", dotted=True)) == [ ('name', 'foo.bar', 1), ] def test_template_string(): assert list(jslexer.tokenize("gettext `foo\"bar\"p`", template_string=True)) == [ ('name', 'gettext', 1), ('template_string', '`foo"bar"p`', 1), ] def test_jsx(): assert list(jslexer.tokenize(""" } data={{active: true}}> """, jsx=True)) == [ ('jsx_tag', '', 2), ('operator', '{', 2), ('name', 'i18n._', 2), ('operator', '(', 2), ('string', "'String1'", 2), ('operator', ')', 2), ('operator', '}', 2), ('jsx_tag', '', 2), ('jsx_tag', '', 3), ('operator', '{', 3), ('name', 'i18n._', 3), ('operator', '(', 3), ('string', "'String 2'", 3), ('operator', ')', 3), ('operator', '}', 3), ('jsx_tag', '', 3), ('jsx_tag', '', 4), ('operator', '{', 4), ('name', 'i18n._', 4), ('operator', '(', 4), ('string', "'String 3'", 4), ('operator', ')', 4), ('operator', '}', 4), ('jsx_tag', '', 4), ('jsx_tag', '', 5), ('jsx_tag', '', 6), ('operator', '}', 6), ('name', 'data', 6), ('operator', '=', 6), ('operator', '{', 6), ('operator', '{', 6), ('name', 'active', 6), ('operator', ':', 6), ('name', 'true', 6), ('operator', '}', 6), ('operator', '}', 6), ('operator', '>', 6), ('jsx_tag', '', 7), ('jsx_tag', '', 8), ] babel-2.14.0/tests/messages/data/0000755000175000017500000000000014536056757016075 5ustar nileshnileshbabel-2.14.0/tests/messages/data/setup.cfg0000644000175000017500000000025414536056757017717 0ustar nileshnilesh[extract_messages] msgid_bugs_address = bugs.address@email.tld copyright_holder = FooBar, TM add_comments = TRANSLATOR:,TRANSLATORS: output_file = project/i18n/project.pot babel-2.14.0/tests/messages/data/project/0000755000175000017500000000000014536056757017543 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/ignored/0000755000175000017500000000000014536056757021172 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/ignored/a_test_file.txt0000644000175000017500000000002214536056757024203 0ustar nileshnileshJust a test file. babel-2.14.0/tests/messages/data/project/ignored/an_example.txt0000644000175000017500000000000014536056757024032 0ustar nileshnileshbabel-2.14.0/tests/messages/data/project/ignored/this_wont_normally_be_here.py0000644000175000017500000000043514536056757027152 0ustar nileshnilesh# -*- coding: utf-8 -*- # This file won't normally be in this directory. # It IS only for tests from gettext import ngettext def foo(): # Note: This will have the TRANSLATOR: tag but shouldn't # be included on the extracted stuff print(ngettext('FooBar', 'FooBars', 1)) babel-2.14.0/tests/messages/data/project/file2.py0000644000175000017500000000035114536056757021115 0ustar nileshnilesh# -*- coding: utf-8 -*- # file2.py for tests from gettext import ngettext def foo(): # Note: This will have the TRANSLATOR: tag but shouldn't # be included on the extracted stuff print(ngettext('foobar', 'foobars', 1)) babel-2.14.0/tests/messages/data/project/i18n/0000755000175000017500000000000014536056757020322 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/messages.pot0000644000175000017500000000146714536056757022665 0ustar nileshnilesh# Translations template for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.1\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" babel-2.14.0/tests/messages/data/project/i18n/de_DE/0000755000175000017500000000000014536056757021262 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/de_DE/LC_MESSAGES/0000755000175000017500000000000014536056757023047 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/de_DE/LC_MESSAGES/foo.po0000644000175000017500000000160314536056757024172 0ustar nileshnilesh# German (Germany) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: 2007-07-30 22:18+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: de_DE \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9dev-r245\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "Stange" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "Fuhstange" msgstr[1] "Fuhstangen" babel-2.14.0/tests/messages/data/project/i18n/de_DE/LC_MESSAGES/bar.po0000644000175000017500000000160314536056757024153 0ustar nileshnilesh# German (Germany) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: 2007-07-30 22:18+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: de_DE \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9dev-r245\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "Stange" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "Fuhstange" msgstr[1] "Fuhstangen" babel-2.14.0/tests/messages/data/project/i18n/de_DE/LC_MESSAGES/messages.po0000644000175000017500000000156314536056757025223 0ustar nileshnilesh# German (Germany) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: 2007-07-30 22:18+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: de_DE \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9dev-r245\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" babel-2.14.0/tests/messages/data/project/i18n/messages_non_fuzzy.pot0000644000175000017500000000145614536056757025004 0ustar nileshnilesh# Translations template for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.1\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" babel-2.14.0/tests/messages/data/project/i18n/de/0000755000175000017500000000000014536056757020712 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/de/LC_MESSAGES/0000755000175000017500000000000014536056757022477 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/de/LC_MESSAGES/messages.mo0000644000175000017500000000104314536056757024641 0ustar nileshnilesh4L`aetbarfoobarfoobarsProject-Id-Version: TestProject 0.1 Report-Msgid-Bugs-To: bugs.address@email.tld POT-Creation-Date: 2007-04-01 15:30+0200 PO-Revision-Date: 2007-07-30 22:18+0200 Last-Translator: FULL NAME Language-Team: de_DE Plural-Forms: nplurals=2; plural=(n != 1) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 0.9dev-r245 StangeFuhstangeFuhstangenbabel-2.14.0/tests/messages/data/project/i18n/de/LC_MESSAGES/messages.po0000644000175000017500000000160314536056757024646 0ustar nileshnilesh# German (Germany) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: 2007-07-30 22:18+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: de_DE \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9dev-r245\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "Stange" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "Fuhstange" msgstr[1] "Fuhstangen" babel-2.14.0/tests/messages/data/project/i18n/ru_RU/0000755000175000017500000000000014536056757021356 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/ru_RU/LC_MESSAGES/0000755000175000017500000000000014536056757023143 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/ru_RU/LC_MESSAGES/messages.po0000644000175000017500000000171414536056757025315 0ustar nileshnilesh# Russian (Russia) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: 2007-07-30 22:18+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: ru_RU \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9dev-r363\n" #. This will be a translator coment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" babel-2.14.0/tests/messages/data/project/i18n/fi_BUGGY/0000755000175000017500000000000014536056757021655 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/0000755000175000017500000000000014536056757023442 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/messages.po0000644000175000017500000000010314536056757025603 0ustar nileshnileshmsgid "" msgstr "" msgid "bar %(sign)s" msgstr "tanko %(merkki)s" babel-2.14.0/tests/messages/data/project/_hidden_by_default/0000755000175000017500000000000014536056757023333 5ustar nileshnileshbabel-2.14.0/tests/messages/data/project/_hidden_by_default/hidden_file.py0000644000175000017500000000011214536056757026131 0ustar nileshnileshfrom gettext import gettext def foo(): print(gettext('ssshhh....')) babel-2.14.0/tests/messages/data/project/__init__.py0000644000175000017500000000000014536056757021642 0ustar nileshnileshbabel-2.14.0/tests/messages/data/project/file1.py0000644000175000017500000000031014536056757021107 0ustar nileshnilesh# -*- coding: utf-8 -*- # file1.py for tests from gettext import gettext as _ def foo(): # TRANSLATOR: This will be a translator coment, # that will include several lines print(_('bar')) babel-2.14.0/tests/messages/data/setup.py0000755000175000017500000000146714536056757017622 0ustar nileshnilesh#!/usr/bin/env python # -*- coding: utf-8 -*- # vim: sw=4 ts=4 fenc=utf-8 # ============================================================================= # $Id$ # ============================================================================= # $URL$ # $LastChangedDate$ # $Rev$ # $LastChangedBy$ # ============================================================================= # Copyright (C) 2006 Ufsoft.org - Pedro Algarvio # # Please view LICENSE for additional licensing information. # ============================================================================= # THIS IS A BOGUS PROJECT from setuptools import setup, find_packages setup( name = 'TestProject', version = '0.1', license = 'BSD', author = 'Foo Bar', author_email = 'foo@bar.tld', packages = find_packages(), ) babel-2.14.0/tests/messages/data/mapping.cfg0000644000175000017500000000014414536056757020210 0ustar nileshnilesh# Ignore directory [ignore: **/ignored/**.*] # Extraction from Python source files [python: **.py] babel-2.14.0/tests/messages/test_normalized_string.py0000644000175000017500000000117414536056757022332 0ustar nileshnileshfrom babel.messages.pofile import _NormalizedString def test_normalized_string(): ab1 = _NormalizedString('a', 'b ') ab2 = _NormalizedString('a', ' b') ac1 = _NormalizedString('a', 'c') ac2 = _NormalizedString(' a', 'c ') z = _NormalizedString() assert ab1 == ab2 and ac1 == ac2 # __eq__ assert ab1 < ac1 # __lt__ assert ac1 > ab2 # __gt__ assert ac1 >= ac2 # __ge__ assert ab1 <= ab2 # __le__ assert ab1 != ac1 # __ne__ assert not z # __nonzero__ / __bool__ assert sorted([ab1, ab2, ac1]) # the sort order is not stable so we can't really check it, just that we can sort babel-2.14.0/tests/messages/test_checkers.py0000644000175000017500000002525514536056757020375 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import unittest from datetime import datetime from io import BytesIO from babel import __version__ as VERSION from babel.core import Locale, UnknownLocaleError from babel.dates import format_datetime from babel.messages import checkers from babel.messages.plurals import PLURALS from babel.messages.pofile import read_po from babel.util import LOCALTZ class CheckersTestCase(unittest.TestCase): # the last msgstr[idx] is always missing except for singular plural forms def test_1_num_plurals_checkers(self): for _locale in [p for p in PLURALS if PLURALS[p][0] == 1]: try: locale = Locale.parse(_locale) except UnknownLocaleError: # Just an alias? Not what we're testing here, let's continue continue date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale) plural = PLURALS[_locale][0] po_file = (f"""\ # {locale.english_name} translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\\n" "POT-Creation-Date: 2007-04-01 15:30+0200\\n" "PO-Revision-Date: {date}\\n" "Last-Translator: FULL NAME \\n" "Language-Team: {_locale} \n" "Plural-Forms: nplurals={plural}; plural={plural};\\n" "MIME-Version: 1.0\\n" "Content-Type: text/plain; charset=utf-8\\n" "Content-Transfer-Encoding: 8bit\\n" "Generated-By: Babel {VERSION}\\n" #. This will be a translator comment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" """).encode('utf-8') # This test will fail for revisions <= 406 because so far # catalog.num_plurals was neglected catalog = read_po(BytesIO(po_file), _locale) message = catalog['foobar'] checkers.num_plurals(catalog, message) def test_2_num_plurals_checkers(self): # in this testcase we add an extra msgstr[idx], we should be # disregarding it for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]: if _locale in ['nn', 'no']: _locale = 'nn_NO' num_plurals = PLURALS[_locale.split('_')[0]][0] plural_expr = PLURALS[_locale.split('_')[0]][1] else: num_plurals = PLURALS[_locale][0] plural_expr = PLURALS[_locale][1] try: locale = Locale(_locale) date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale) except UnknownLocaleError: # Just an alias? Not what we're testing here, let's continue continue po_file = f"""\ # {locale.english_name} translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\\n" "POT-Creation-Date: 2007-04-01 15:30+0200\\n" "PO-Revision-Date: {date}\\n" "Last-Translator: FULL NAME \\n" "Language-Team: {_locale} \\n" "Plural-Forms: nplurals={num_plurals}; plural={plural_expr};\\n" "MIME-Version: 1.0\\n" "Content-Type: text/plain; charset=utf-8\\n" "Content-Transfer-Encoding: 8bit\\n" "Generated-By: Babel {VERSION}\\n" #. This will be a translator comment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" """.encode('utf-8') # we should be adding the missing msgstr[0] # This test will fail for revisions <= 406 because so far # catalog.num_plurals was neglected catalog = read_po(BytesIO(po_file), _locale) message = catalog['foobar'] checkers.num_plurals(catalog, message) def test_3_num_plurals_checkers(self): for _locale in [p for p in PLURALS if PLURALS[p][0] == 3]: plural = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale) english_name = Locale.parse(_locale).english_name po_file = fr"""\ # {english_name} translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {plural}\n" "Last-Translator: FULL NAME \n" "Language-Team: {_locale} \n" "Plural-Forms: nplurals={PLURALS[_locale][0]}; plural={PLURALS[_locale][0]};\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator comment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" """.encode('utf-8') # This test will fail for revisions <= 406 because so far # catalog.num_plurals was neglected catalog = read_po(BytesIO(po_file), _locale) message = catalog['foobar'] checkers.num_plurals(catalog, message) def test_4_num_plurals_checkers(self): for _locale in [p for p in PLURALS if PLURALS[p][0] == 4]: date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale) english_name = Locale.parse(_locale).english_name plural = PLURALS[_locale][0] po_file = fr"""\ # {english_name} translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language-Team: {_locale} \n" "Plural-Forms: nplurals={plural}; plural={plural};\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator comment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" """.encode('utf-8') # This test will fail for revisions <= 406 because so far # catalog.num_plurals was neglected catalog = read_po(BytesIO(po_file), _locale) message = catalog['foobar'] checkers.num_plurals(catalog, message) def test_5_num_plurals_checkers(self): for _locale in [p for p in PLURALS if PLURALS[p][0] == 5]: date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale) english_name = Locale.parse(_locale).english_name plural = PLURALS[_locale][0] po_file = fr"""\ # {english_name} translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language-Team: {_locale} \n" "Plural-Forms: nplurals={plural}; plural={plural};\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator comment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" """.encode('utf-8') # This test will fail for revisions <= 406 because so far # catalog.num_plurals was neglected catalog = read_po(BytesIO(po_file), _locale) message = catalog['foobar'] checkers.num_plurals(catalog, message) def test_6_num_plurals_checkers(self): for _locale in [p for p in PLURALS if PLURALS[p][0] == 6]: english_name = Locale.parse(_locale).english_name date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale) plural = PLURALS[_locale][0] po_file = fr"""\ # {english_name} translations for TestProject. # Copyright (C) 2007 FooBar, Inc. # This file is distributed under the same license as the TestProject # project. # FIRST AUTHOR , 2007. # msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" "Report-Msgid-Bugs-To: bugs.address@email.tld\n" "POT-Creation-Date: 2007-04-01 15:30+0200\n" "PO-Revision-Date: {date}\n" "Last-Translator: FULL NAME \n" "Language-Team: {_locale} \n" "Plural-Forms: nplurals={plural}; plural={plural};\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel {VERSION}\n" #. This will be a translator comment, #. that will include several lines #: project/file1.py:8 msgid "bar" msgstr "" #: project/file2.py:9 msgid "foobar" msgid_plural "foobars" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" """.encode('utf-8') # This test will fail for revisions <= 406 because so far # catalog.num_plurals was neglected catalog = read_po(BytesIO(po_file), _locale) message = catalog['foobar'] checkers.num_plurals(catalog, message) babel-2.14.0/tests/messages/test_mofile.py0000644000175000017500000000633614536056757020060 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import os import unittest from io import BytesIO from babel.messages import Catalog, mofile from babel.support import Translations class ReadMoTestCase(unittest.TestCase): def setUp(self): self.datadir = os.path.join(os.path.dirname(__file__), 'data') def test_basics(self): mo_path = os.path.join(self.datadir, 'project', 'i18n', 'de', 'LC_MESSAGES', 'messages.mo') with open(mo_path, 'rb') as mo_file: catalog = mofile.read_mo(mo_file) assert len(catalog) == 2 assert catalog.project == 'TestProject' assert catalog.version == '0.1' assert catalog['bar'].string == 'Stange' assert catalog['foobar'].string == ['Fuhstange', 'Fuhstangen'] class WriteMoTestCase(unittest.TestCase): def test_sorting(self): # Ensure the header is sorted to the first entry so that its charset # can be applied to all subsequent messages by GNUTranslations # (ensuring all messages are safely converted to unicode) catalog = Catalog(locale='en_US') catalog.add('', '''\ "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n''') catalog.add('foo', 'Voh') catalog.add(('There is', 'There are'), ('Es gibt', 'Es gibt')) catalog.add('Fizz', '') catalog.add(('Fuzz', 'Fuzzes'), ('', '')) buf = BytesIO() mofile.write_mo(buf, catalog) buf.seek(0) translations = Translations(fp=buf) assert translations.ugettext('foo') == 'Voh' assert translations.ungettext('There is', 'There are', 1) == 'Es gibt' assert translations.ugettext('Fizz') == 'Fizz' assert translations.ugettext('Fuzz') == 'Fuzz' assert translations.ugettext('Fuzzes') == 'Fuzzes' def test_more_plural_forms(self): catalog2 = Catalog(locale='ru_RU') catalog2.add(('Fuzz', 'Fuzzes'), ('', '', '')) buf = BytesIO() mofile.write_mo(buf, catalog2) def test_empty_translation_with_fallback(self): catalog1 = Catalog(locale='fr_FR') catalog1.add('', '''\ "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n''') catalog1.add('Fuzz', '') buf1 = BytesIO() mofile.write_mo(buf1, catalog1) buf1.seek(0) catalog2 = Catalog(locale='fr') catalog2.add('', '''\ "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n''') catalog2.add('Fuzz', 'Flou') buf2 = BytesIO() mofile.write_mo(buf2, catalog2) buf2.seek(0) translations = Translations(fp=buf1) translations.add_fallback(Translations(fp=buf2)) assert translations.ugettext('Fuzz') == 'Flou' babel-2.14.0/tests/messages/test_extract.py0000644000175000017500000005000414536056757020246 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import codecs import sys import unittest from io import BytesIO, StringIO import pytest from babel.messages import extract class ExtractPythonTestCase(unittest.TestCase): def test_nested_calls(self): buf = BytesIO(b"""\ msg1 = _(i18n_arg.replace(r'\"', '"')) msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2) msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2) msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2) msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2)) msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2)) msg7 = _(hello.there) msg8 = gettext('Rabbit') msg9 = dgettext('wiki', model.addPage()) msg10 = dngettext(getDomain(), 'Page', 'Pages', 3) """) messages = list(extract.extract_python(buf, extract.DEFAULT_KEYWORDS.keys(), [], {})) assert messages == [ (1, '_', None, []), (2, 'ungettext', (None, None, None), []), (3, 'ungettext', ('Babel', None, None), []), (4, 'ungettext', (None, 'Babels', None), []), (5, 'ungettext', ('bunny', 'bunnies', None), []), (6, 'ungettext', (None, 'bunnies', None), []), (7, '_', None, []), (8, 'gettext', 'Rabbit', []), (9, 'dgettext', ('wiki', None), []), (10, 'dngettext', (None, 'Page', 'Pages', None), []), ] def test_extract_default_encoding_ascii(self): buf = BytesIO(b'_("a")') messages = list(extract.extract_python( buf, list(extract.DEFAULT_KEYWORDS), [], {}, )) # Should work great in both py2 and py3 assert messages == [(1, '_', 'a', [])] def test_extract_default_encoding_utf8(self): buf = BytesIO('_("☃")'.encode('UTF-8')) messages = list(extract.extract_python( buf, list(extract.DEFAULT_KEYWORDS), [], {}, )) assert messages == [(1, '_', '☃', [])] def test_nested_comments(self): buf = BytesIO(b"""\ msg = ngettext('pylon', # TRANSLATORS: shouldn't be 'pylons', # TRANSLATORS: seeing this count) """) messages = list(extract.extract_python(buf, ('ngettext',), ['TRANSLATORS:'], {})) assert messages == [(1, 'ngettext', ('pylon', 'pylons', None), [])] def test_comments_with_calls_that_spawn_multiple_lines(self): buf = BytesIO(b"""\ # NOTE: This Comment SHOULD Be Extracted add_notice(req, ngettext("Catalog deleted.", "Catalogs deleted.", len(selected))) # NOTE: This Comment SHOULD Be Extracted add_notice(req, _("Locale deleted.")) # NOTE: This Comment SHOULD Be Extracted add_notice(req, ngettext("Foo deleted.", "Foos deleted.", len(selected))) # NOTE: This Comment SHOULD Be Extracted # NOTE: And This One Too add_notice(req, ngettext("Bar deleted.", "Bars deleted.", len(selected))) """) messages = list(extract.extract_python(buf, ('ngettext', '_'), ['NOTE:'], {'strip_comment_tags': False})) assert messages[0] == (3, 'ngettext', ('Catalog deleted.', 'Catalogs deleted.', None), ['NOTE: This Comment SHOULD Be Extracted']) assert messages[1] == (6, '_', 'Locale deleted.', ['NOTE: This Comment SHOULD Be Extracted']) assert messages[2] == (10, 'ngettext', ('Foo deleted.', 'Foos deleted.', None), ['NOTE: This Comment SHOULD Be Extracted']) assert messages[3] == (15, 'ngettext', ('Bar deleted.', 'Bars deleted.', None), ['NOTE: This Comment SHOULD Be Extracted', 'NOTE: And This One Too']) def test_declarations(self): buf = BytesIO(b"""\ class gettext(object): pass def render_body(context,x,y=_('Page arg 1'),z=_('Page arg 2'),**pageargs): pass def ngettext(y='arg 1',z='arg 2',**pageargs): pass class Meta: verbose_name = _('log entry') """) messages = list(extract.extract_python(buf, extract.DEFAULT_KEYWORDS.keys(), [], {})) assert messages == [ (3, '_', 'Page arg 1', []), (3, '_', 'Page arg 2', []), (8, '_', 'log entry', []), ] def test_multiline(self): buf = BytesIO(b"""\ msg1 = ngettext('pylon', 'pylons', count) msg2 = ngettext('elvis', 'elvises', count) """) messages = list(extract.extract_python(buf, ('ngettext',), [], {})) assert messages == [ (1, 'ngettext', ('pylon', 'pylons', None), []), (3, 'ngettext', ('elvis', 'elvises', None), []), ] def test_npgettext(self): buf = BytesIO(b"""\ msg1 = npgettext('Strings','pylon', 'pylons', count) msg2 = npgettext('Strings','elvis', 'elvises', count) """) messages = list(extract.extract_python(buf, ('npgettext',), [], {})) assert messages == [ (1, 'npgettext', ('Strings', 'pylon', 'pylons', None), []), (3, 'npgettext', ('Strings', 'elvis', 'elvises', None), []), ] buf = BytesIO(b"""\ msg = npgettext('Strings', 'pylon', # TRANSLATORS: shouldn't be 'pylons', # TRANSLATORS: seeing this count) """) messages = list(extract.extract_python(buf, ('npgettext',), ['TRANSLATORS:'], {})) assert messages == [ (1, 'npgettext', ('Strings', 'pylon', 'pylons', None), []), ] def test_triple_quoted_strings(self): buf = BytesIO(b"""\ msg1 = _('''pylons''') msg2 = ngettext(r'''elvis''', \"\"\"elvises\"\"\", count) msg2 = ngettext(\"\"\"elvis\"\"\", 'elvises', count) """) messages = list(extract.extract_python(buf, extract.DEFAULT_KEYWORDS.keys(), [], {})) assert messages == [ (1, '_', 'pylons', []), (2, 'ngettext', ('elvis', 'elvises', None), []), (3, 'ngettext', ('elvis', 'elvises', None), []), ] def test_multiline_strings(self): buf = BytesIO(b"""\ _('''This module provides internationalization and localization support for your Python programs by providing an interface to the GNU gettext message catalog library.''') """) messages = list(extract.extract_python(buf, extract.DEFAULT_KEYWORDS.keys(), [], {})) assert messages == [ (1, '_', 'This module provides internationalization and localization\n' 'support for your Python programs by providing an interface to ' 'the GNU\ngettext message catalog library.', []), ] def test_concatenated_strings(self): buf = BytesIO(b"""\ foobar = _('foo' 'bar') """) messages = list(extract.extract_python(buf, extract.DEFAULT_KEYWORDS.keys(), [], {})) assert messages[0][2] == 'foobar' def test_unicode_string_arg(self): buf = BytesIO(b"msg = _(u'Foo Bar')") messages = list(extract.extract_python(buf, ('_',), [], {})) assert messages[0][2] == 'Foo Bar' def test_comment_tag(self): buf = BytesIO(b""" # NOTE: A translation comment msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == ['NOTE: A translation comment'] def test_comment_tag_multiline(self): buf = BytesIO(b""" # NOTE: A translation comment # with a second line msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == ['NOTE: A translation comment', 'with a second line'] def test_translator_comments_with_previous_non_translator_comments(self): buf = BytesIO(b""" # This shouldn't be in the output # because it didn't start with a comment tag # NOTE: A translation comment # with a second line msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == ['NOTE: A translation comment', 'with a second line'] def test_comment_tags_not_on_start_of_comment(self): buf = BytesIO(b""" # This shouldn't be in the output # because it didn't start with a comment tag # do NOTE: this will not be a translation comment # NOTE: This one will be msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == ['NOTE: This one will be'] def test_multiple_comment_tags(self): buf = BytesIO(b""" # NOTE1: A translation comment for tag1 # with a second line msg = _(u'Foo Bar1') # NOTE2: A translation comment for tag2 msg = _(u'Foo Bar2') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE1:', 'NOTE2:'], {})) assert messages[0][2] == 'Foo Bar1' assert messages[0][3] == ['NOTE1: A translation comment for tag1', 'with a second line'] assert messages[1][2] == 'Foo Bar2' assert messages[1][3] == ['NOTE2: A translation comment for tag2'] def test_two_succeeding_comments(self): buf = BytesIO(b""" # NOTE: one # NOTE: two msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == ['NOTE: one', 'NOTE: two'] def test_invalid_translator_comments(self): buf = BytesIO(b""" # NOTE: this shouldn't apply to any messages hello = 'there' msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == [] def test_invalid_translator_comments2(self): buf = BytesIO(b""" # NOTE: Hi! hithere = _('Hi there!') # NOTE: you should not be seeing this in the .po rows = [[v for v in range(0,10)] for row in range(0,10)] # this (NOTE:) should not show up either hello = _('Hello') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Hi there!' assert messages[0][3] == ['NOTE: Hi!'] assert messages[1][2] == 'Hello' assert messages[1][3] == [] def test_invalid_translator_comments3(self): buf = BytesIO(b""" # NOTE: Hi, # there! hithere = _('Hi there!') """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Hi there!' assert messages[0][3] == [] def test_comment_tag_with_leading_space(self): buf = BytesIO(b""" #: A translation comment #: with leading spaces msg = _(u'Foo Bar') """) messages = list(extract.extract_python(buf, ('_',), [':'], {})) assert messages[0][2] == 'Foo Bar' assert messages[0][3] == [': A translation comment', ': with leading spaces'] def test_different_signatures(self): buf = BytesIO(b""" foo = _('foo', 'bar') n = ngettext('hello', 'there', n=3) n = ngettext(n=3, 'hello', 'there') n = ngettext(n=3, *messages) n = ngettext() n = ngettext('foo') """) messages = list(extract.extract_python(buf, ('_', 'ngettext'), [], {})) assert messages[0][2] == ('foo', 'bar') assert messages[1][2] == ('hello', 'there', None) assert messages[2][2] == (None, 'hello', 'there') assert messages[3][2] == (None, None) assert messages[4][2] is None assert messages[5][2] == 'foo' def test_utf8_message(self): buf = BytesIO(""" # NOTE: hello msg = _('Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {'encoding': 'utf-8'})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][3] == ['NOTE: hello'] def test_utf8_message_with_magic_comment(self): buf = BytesIO("""# -*- coding: utf-8 -*- # NOTE: hello msg = _('Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][3] == ['NOTE: hello'] def test_utf8_message_with_utf8_bom(self): buf = BytesIO(codecs.BOM_UTF8 + """ # NOTE: hello msg = _('Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][3] == ['NOTE: hello'] def test_utf8_message_with_utf8_bom_and_magic_comment(self): buf = BytesIO(codecs.BOM_UTF8 + """# -*- coding: utf-8 -*- # NOTE: hello msg = _('Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][3] == ['NOTE: hello'] def test_utf8_bom_with_latin_magic_comment_fails(self): buf = BytesIO(codecs.BOM_UTF8 + """# -*- coding: latin-1 -*- # NOTE: hello msg = _('Bonjour à tous') """.encode('utf-8')) with pytest.raises(SyntaxError): list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) def test_utf8_raw_strings_match_unicode_strings(self): buf = BytesIO(codecs.BOM_UTF8 + """ msg = _('Bonjour à tous') msgu = _(u'Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][2] == messages[1][2] def test_extract_strip_comment_tags(self): buf = BytesIO(b"""\ #: This is a comment with a very simple #: prefix specified _('Servus') # NOTE: This is a multiline comment with # a prefix too _('Babatschi')""") messages = list(extract.extract('python', buf, comment_tags=['NOTE:', ':'], strip_comment_tags=True)) assert messages[0][1] == 'Servus' assert messages[0][2] == ['This is a comment with a very simple', 'prefix specified'] assert messages[1][1] == 'Babatschi' assert messages[1][2] == ['This is a multiline comment with', 'a prefix too'] def test_nested_messages(self): buf = BytesIO(b""" # NOTE: First _(u'Hello, {name}!', name=_(u'Foo Bar')) # NOTE: Second _(u'Hello, {name1} and {name2}!', name1=_(u'Heungsub'), name2=_(u'Armin')) # NOTE: Third _(u'Hello, {0} and {1}!', _(u'Heungsub'), _(u'Armin')) """) messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == ('Hello, {name}!', None) assert messages[0][3] == ['NOTE: First'] assert messages[1][2] == 'Foo Bar' assert messages[1][3] == [] assert messages[2][2] == ('Hello, {name1} and {name2}!', None) assert messages[2][3] == ['NOTE: Second'] assert messages[3][2] == 'Heungsub' assert messages[3][3] == [] assert messages[4][2] == 'Armin' assert messages[4][3] == [] assert messages[5][2] == ('Hello, {0} and {1}!', None) assert messages[5][3] == ['NOTE: Third'] assert messages[6][2] == 'Heungsub' assert messages[6][3] == [] assert messages[7][2] == 'Armin' assert messages[7][3] == [] class ExtractTestCase(unittest.TestCase): def test_invalid_filter(self): buf = BytesIO(b"""\ msg1 = _(i18n_arg.replace(r'\"', '"')) msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2) msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2) msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2) msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2)) msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2)) msg7 = _(hello.there) msg8 = gettext('Rabbit') msg9 = dgettext('wiki', model.addPage()) msg10 = dngettext(domain, 'Page', 'Pages', 3) """) messages = \ list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {})) assert messages == [ (5, ('bunny', 'bunnies'), [], None), (8, 'Rabbit', [], None), (10, ('Page', 'Pages'), [], None), ] def test_invalid_extract_method(self): buf = BytesIO(b'') with pytest.raises(ValueError): list(extract.extract('spam', buf)) def test_different_signatures(self): buf = BytesIO(b""" foo = _('foo', 'bar') n = ngettext('hello', 'there', n=3) n = ngettext(n=3, 'hello', 'there') n = ngettext(n=3, *messages) n = ngettext() n = ngettext('foo') """) messages = \ list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {})) assert len(messages) == 2 assert messages[0][1] == 'foo' assert messages[1][1] == ('hello', 'there') def test_empty_string_msgid(self): buf = BytesIO(b"""\ msg = _('') """) stderr = sys.stderr sys.stderr = StringIO() try: messages = \ list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {})) assert messages == [] assert 'warning: Empty msgid.' in sys.stderr.getvalue() finally: sys.stderr = stderr def test_warn_if_empty_string_msgid_found_in_context_aware_extraction_method(self): buf = BytesIO(b"\nmsg = pgettext('ctxt', '')\n") stderr = sys.stderr sys.stderr = StringIO() try: messages = extract.extract('python', buf) assert list(messages) == [] assert 'warning: Empty msgid.' in sys.stderr.getvalue() finally: sys.stderr = stderr def test_extract_allows_callable(self): def arbitrary_extractor(fileobj, keywords, comment_tags, options): return [(1, None, (), ())] for x in extract.extract(arbitrary_extractor, BytesIO(b"")): assert x[0] == 1 def test_future(self): buf = BytesIO(br""" # -*- coding: utf-8 -*- from __future__ import unicode_literals nbsp = _('\xa0') """) messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {})) assert messages[0][1] == '\xa0' def test_f_strings(self): buf = BytesIO(br""" t1 = _('foobar') t2 = _(f'spameggs' f'feast') # should be extracted; constant parts only t2 = _(f'spameggs' 'kerroshampurilainen') # should be extracted (mixing f with no f) t3 = _(f'''whoa! a ''' # should be extracted (continues on following lines) f'flying shark' '... hello' ) t4 = _(f'spameggs {t1}') # should not be extracted """) messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {})) assert len(messages) == 4 assert messages[0][1] == 'foobar' assert messages[1][1] == 'spameggsfeast' assert messages[2][1] == 'spameggskerroshampurilainen' assert messages[3][1] == 'whoa! a flying shark... hello' def test_f_strings_non_utf8(self): buf = BytesIO(b""" # -- coding: latin-1 -- t2 = _(f'\xe5\xe4\xf6' f'\xc5\xc4\xd6') """) messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {})) assert len(messages) == 1 assert messages[0][1] == 'åäöÅÄÖ' babel-2.14.0/tests/messages/test_setuptools_frontend.py0000644000175000017500000000642614536056757022725 0ustar nileshnileshimport os import shlex import shutil import subprocess import sys from pathlib import Path import pytest from tests.messages.consts import data_dir Distribution = pytest.importorskip("setuptools").Distribution @pytest.mark.parametrize("kwarg,expected", [ ("LW_", ("LW_",)), ("LW_ QQ Q", ("LW_", "QQ", "Q")), ("yiy aia", ("yiy", "aia")), ]) def test_extract_distutils_keyword_arg_388(kwarg, expected): from babel.messages import frontend, setuptools_frontend # This is a regression test for https://github.com/python-babel/babel/issues/388 # Note that distutils-based commands only support a single repetition of the same argument; # hence `--keyword ignored` will actually never end up in the output. cmdline = ( "extract_messages --no-default-keywords --keyword ignored --keyword '%s' " "--input-dirs . --output-file django233.pot --add-comments Bar,Foo" % kwarg ) d = Distribution(attrs={ "cmdclass": setuptools_frontend.COMMANDS, "script_args": shlex.split(cmdline), }) d.parse_command_line() assert len(d.commands) == 1 cmdinst = d.get_command_obj(d.commands[0]) cmdinst.ensure_finalized() assert isinstance(cmdinst, frontend.ExtractMessages) assert isinstance(cmdinst, setuptools_frontend.extract_messages) assert set(cmdinst.keywords.keys()) == set(expected) # Test the comma-separated comment argument while we're at it: assert set(cmdinst.add_comments) == {"Bar", "Foo"} def test_setuptools_commands(tmp_path, monkeypatch): """ Smoke-tests all of the setuptools versions of the commands in turn. Their full functionality is tested better in `test_frontend.py`. """ # Copy the test project to a temporary directory and work there dest = tmp_path / "dest" shutil.copytree(data_dir, dest) monkeypatch.chdir(dest) env = os.environ.copy() # When in Tox, we need to hack things a bit so as not to have the # sub-interpreter `sys.executable` use the tox virtualenv's Babel # installation, so the locale data is where we expect it to be. if "BABEL_TOX_INI_DIR" in env: env["PYTHONPATH"] = env["BABEL_TOX_INI_DIR"] # Initialize an empty catalog subprocess.check_call([ sys.executable, "setup.py", "init_catalog", "-i", os.devnull, "-l", "fi", "-d", "inited", ], env=env) po_file = Path("inited/fi/LC_MESSAGES/messages.po") orig_po_data = po_file.read_text() subprocess.check_call([ sys.executable, "setup.py", "extract_messages", "-o", "extracted.pot", ], env=env) pot_file = Path("extracted.pot") pot_data = pot_file.read_text() assert "FooBar, TM" in pot_data # should be read from setup.cfg assert "bugs.address@email.tld" in pot_data # should be read from setup.cfg subprocess.check_call([ sys.executable, "setup.py", "update_catalog", "-i", "extracted.pot", "-d", "inited", ], env=env) new_po_data = po_file.read_text() assert new_po_data != orig_po_data # check we updated the file subprocess.check_call([ sys.executable, "setup.py", "compile_catalog", "-d", "inited", ], env=env) assert po_file.with_suffix(".mo").exists() babel-2.14.0/tests/messages/__init__.py0000644000175000017500000000000014536056757017263 0ustar nileshnileshbabel-2.14.0/tests/messages/test_pofile.py0000644000175000017500000007073514536056757020067 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import unittest from datetime import datetime from io import BytesIO, StringIO import pytest from babel.core import Locale from babel.messages import pofile from babel.messages.catalog import Catalog, Message from babel.util import FixedOffsetTimezone class ReadPoTestCase(unittest.TestCase): def test_preserve_locale(self): buf = StringIO(r'''msgid "foo" msgstr "Voh"''') catalog = pofile.read_po(buf, locale='en_US') assert Locale('en', 'US') == catalog.locale def test_locale_gets_overridden_by_file(self): buf = StringIO(r''' msgid "" msgstr "" "Language: en_US\n"''') catalog = pofile.read_po(buf, locale='de') assert Locale('en', 'US') == catalog.locale buf = StringIO(r''' msgid "" msgstr "" "Language: ko-KR\n"''') catalog = pofile.read_po(buf, locale='de') assert Locale('ko', 'KR') == catalog.locale def test_preserve_domain(self): buf = StringIO(r'''msgid "foo" msgstr "Voh"''') catalog = pofile.read_po(buf, domain='mydomain') assert catalog.domain == 'mydomain' def test_applies_specified_encoding_during_read(self): buf = BytesIO(''' msgid "" msgstr "" "Project-Id-Version: 3.15\\n" "Report-Msgid-Bugs-To: Fliegender Zirkus \\n" "POT-Creation-Date: 2007-09-27 11:19+0700\\n" "PO-Revision-Date: 2007-09-27 21:42-0700\\n" "Last-Translator: John \\n" "Language-Team: German Lang \\n" "Plural-Forms: nplurals=2; plural=(n != 1);\\n" "MIME-Version: 1.0\\n" "Content-Type: text/plain; charset=iso-8859-1\\n" "Content-Transfer-Encoding: 8bit\\n" "Generated-By: Babel 1.0dev-r313\\n" msgid "foo" msgstr "bär"'''.encode('iso-8859-1')) catalog = pofile.read_po(buf, locale='de_DE') assert catalog.get('foo').string == 'bär' def test_encoding_header_read(self): buf = BytesIO(b'msgid ""\nmsgstr ""\n"Content-Type: text/plain; charset=mac_roman\\n"\n') catalog = pofile.read_po(buf, locale='xx_XX') assert catalog.charset == 'mac_roman' def test_plural_forms_header_parsed(self): buf = BytesIO(b'msgid ""\nmsgstr ""\n"Plural-Forms: nplurals=42; plural=(n % 11);\\n"\n') catalog = pofile.read_po(buf, locale='xx_XX') assert catalog.plural_expr == '(n % 11)' assert catalog.num_plurals == 42 def test_read_multiline(self): buf = StringIO(r'''msgid "" "Here's some text that\n" "includesareallylongwordthatmightbutshouldnt" " throw us into an infinite " "loop\n" msgstr ""''') catalog = pofile.read_po(buf) assert len(catalog) == 1 message = list(catalog)[1] assert message.id == ( "Here's some text that\nincludesareallylongwordthat" "mightbutshouldnt throw us into an infinite loop\n" ) def test_fuzzy_header(self): buf = StringIO(r''' # Translations template for AReallyReallyLongNameForAProject. # Copyright (C) 2007 ORGANIZATION # This file is distributed under the same license as the # AReallyReallyLongNameForAProject project. # FIRST AUTHOR , 2007. # #, fuzzy ''') catalog = pofile.read_po(buf) assert len(list(catalog)) == 1 assert list(catalog)[0].fuzzy def test_not_fuzzy_header(self): buf = StringIO(r''' # Translations template for AReallyReallyLongNameForAProject. # Copyright (C) 2007 ORGANIZATION # This file is distributed under the same license as the # AReallyReallyLongNameForAProject project. # FIRST AUTHOR , 2007. # ''') catalog = pofile.read_po(buf) assert len(list(catalog)) == 1 assert not list(catalog)[0].fuzzy def test_header_entry(self): buf = StringIO(r''' # SOME DESCRIPTIVE TITLE. # Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , 2007. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 3.15\n" "Report-Msgid-Bugs-To: Fliegender Zirkus \n" "POT-Creation-Date: 2007-09-27 11:19+0700\n" "PO-Revision-Date: 2007-09-27 21:42-0700\n" "Last-Translator: John \n" "Language: de\n" "Language-Team: German Lang \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=iso-8859-2\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 1.0dev-r313\n" ''') catalog = pofile.read_po(buf) assert len(list(catalog)) == 1 assert catalog.version == '3.15' assert catalog.msgid_bugs_address == 'Fliegender Zirkus ' assert datetime(2007, 9, 27, 11, 19, tzinfo=FixedOffsetTimezone(7 * 60)) == catalog.creation_date assert catalog.last_translator == 'John ' assert Locale('de') == catalog.locale assert catalog.language_team == 'German Lang ' assert catalog.charset == 'iso-8859-2' assert list(catalog)[0].fuzzy def test_obsolete_message(self): buf = StringIO(r'''# This is an obsolete message #~ msgid "foo" #~ msgstr "Voh" # This message is not obsolete #: main.py:1 msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf) assert len(catalog) == 1 assert len(catalog.obsolete) == 1 message = catalog.obsolete['foo'] assert message.id == 'foo' assert message.string == 'Voh' assert message.user_comments == ['This is an obsolete message'] def test_obsolete_message_ignored(self): buf = StringIO(r'''# This is an obsolete message #~ msgid "foo" #~ msgstr "Voh" # This message is not obsolete #: main.py:1 msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf, ignore_obsolete=True) assert len(catalog) == 1 assert len(catalog.obsolete) == 0 def test_multi_line_obsolete_message(self): buf = StringIO(r'''# This is an obsolete message #~ msgid "" #~ "foo" #~ "foo" #~ msgstr "" #~ "Voh" #~ "Vooooh" # This message is not obsolete #: main.py:1 msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf) assert len(catalog.obsolete) == 1 message = catalog.obsolete['foofoo'] assert message.id == 'foofoo' assert message.string == 'VohVooooh' assert message.user_comments == ['This is an obsolete message'] def test_unit_following_multi_line_obsolete_message(self): buf = StringIO(r'''# This is an obsolete message #~ msgid "" #~ "foo" #~ "fooooooo" #~ msgstr "" #~ "Voh" #~ "Vooooh" # This message is not obsolete #: main.py:1 msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf) assert len(catalog) == 1 message = catalog['bar'] assert message.id == 'bar' assert message.string == 'Bahr' assert message.user_comments == ['This message is not obsolete'] def test_unit_before_obsolete_is_not_obsoleted(self): buf = StringIO(r''' # This message is not obsolete #: main.py:1 msgid "bar" msgstr "Bahr" # This is an obsolete message #~ msgid "" #~ "foo" #~ "fooooooo" #~ msgstr "" #~ "Voh" #~ "Vooooh" ''') catalog = pofile.read_po(buf) assert len(catalog) == 1 message = catalog['bar'] assert message.id == 'bar' assert message.string == 'Bahr' assert message.user_comments == ['This message is not obsolete'] def test_with_context(self): buf = BytesIO(b'''# Some string in the menu #: main.py:1 msgctxt "Menu" msgid "foo" msgstr "Voh" # Another string in the menu #: main.py:2 msgctxt "Menu" msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf, ignore_obsolete=True) assert len(catalog) == 2 message = catalog.get('foo', context='Menu') assert message.context == 'Menu' message = catalog.get('bar', context='Menu') assert message.context == 'Menu' # And verify it pass through write_po out_buf = BytesIO() pofile.write_po(out_buf, catalog, omit_header=True) assert out_buf.getvalue().strip() == buf.getvalue().strip() def test_obsolete_message_with_context(self): buf = StringIO(''' # This message is not obsolete msgid "baz" msgstr "Bazczch" # This is an obsolete message #~ msgctxt "other" #~ msgid "foo" #~ msgstr "Voh" # This message is not obsolete #: main.py:1 msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf) assert len(catalog) == 2 assert len(catalog.obsolete) == 1 message = catalog.obsolete["foo"] assert message.context == 'other' assert message.string == 'Voh' def test_multiline_context(self): buf = StringIO(''' msgctxt "a really long " "message context " "why?" msgid "mid" msgstr "mst" ''') catalog = pofile.read_po(buf) assert len(catalog) == 1 message = catalog.get('mid', context="a really long message context why?") assert message is not None assert message.context == 'a really long message context why?' def test_with_context_two(self): buf = BytesIO(b'''msgctxt "Menu" msgid "foo" msgstr "Voh" msgctxt "Mannu" msgid "bar" msgstr "Bahr" ''') catalog = pofile.read_po(buf, ignore_obsolete=True) assert len(catalog) == 2 message = catalog.get('foo', context='Menu') assert message.context == 'Menu' message = catalog.get('bar', context='Mannu') assert message.context == 'Mannu' # And verify it pass through write_po out_buf = BytesIO() pofile.write_po(out_buf, catalog, omit_header=True) assert out_buf.getvalue().strip() == buf.getvalue().strip(), out_buf.getvalue() def test_single_plural_form(self): buf = StringIO(r'''msgid "foo" msgid_plural "foos" msgstr[0] "Voh"''') catalog = pofile.read_po(buf, locale='ja_JP') assert len(catalog) == 1 assert catalog.num_plurals == 1 message = catalog['foo'] assert len(message.string) == 1 def test_singular_plural_form(self): buf = StringIO(r'''msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Vohs"''') catalog = pofile.read_po(buf, locale='nl_NL') assert len(catalog) == 1 assert catalog.num_plurals == 2 message = catalog['foo'] assert len(message.string) == 2 def test_more_than_two_plural_forms(self): buf = StringIO(r'''msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Vohs" msgstr[2] "Vohss"''') catalog = pofile.read_po(buf, locale='lv_LV') assert len(catalog) == 1 assert catalog.num_plurals == 3 message = catalog['foo'] assert len(message.string) == 3 assert message.string[2] == 'Vohss' def test_plural_with_square_brackets(self): buf = StringIO(r'''msgid "foo" msgid_plural "foos" msgstr[0] "Voh [text]" msgstr[1] "Vohs [text]"''') catalog = pofile.read_po(buf, locale='nb_NO') assert len(catalog) == 1 assert catalog.num_plurals == 2 message = catalog['foo'] assert len(message.string) == 2 def test_obsolete_plural_with_square_brackets(self): buf = StringIO('''\ #~ msgid "foo" #~ msgid_plural "foos" #~ msgstr[0] "Voh [text]" #~ msgstr[1] "Vohs [text]" ''') catalog = pofile.read_po(buf, locale='nb_NO') assert len(catalog) == 0 assert len(catalog.obsolete) == 1 assert catalog.num_plurals == 2 message = catalog.obsolete[('foo', 'foos')] assert len(message.string) == 2 assert message.string[0] == 'Voh [text]' assert message.string[1] == 'Vohs [text]' def test_missing_plural(self): buf = StringIO('''\ msgid "" msgstr "" "Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n" msgid "foo" msgid_plural "foos" msgstr[0] "Voh [text]" msgstr[1] "Vohs [text]" ''') catalog = pofile.read_po(buf, locale='nb_NO') assert len(catalog) == 1 assert catalog.num_plurals == 3 message = catalog['foo'] assert len(message.string) == 3 assert message.string[0] == 'Voh [text]' assert message.string[1] == 'Vohs [text]' assert message.string[2] == '' def test_missing_plural_in_the_middle(self): buf = StringIO('''\ msgid "" msgstr "" "Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n" msgid "foo" msgid_plural "foos" msgstr[0] "Voh [text]" msgstr[2] "Vohs [text]" ''') catalog = pofile.read_po(buf, locale='nb_NO') assert len(catalog) == 1 assert catalog.num_plurals == 3 message = catalog['foo'] assert len(message.string) == 3 assert message.string[0] == 'Voh [text]' assert message.string[1] == '' assert message.string[2] == 'Vohs [text]' def test_abort_invalid_po_file(self): invalid_po = ''' msgctxt "" "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": " "270005359}" msgid "" "Thank you very much for your time.\n" "If you have any questions regarding this survey, please contact Fulano " "at nadie@blah.com" msgstr "Merci de prendre le temps de remplir le sondage. Pour toute question, veuillez communiquer avec Fulano à nadie@blah.com " ''' invalid_po_2 = ''' msgctxt "" "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": " "270005359}" msgid "" "Thank you very much for your time.\n" "If you have any questions regarding this survey, please contact Fulano " "at fulano@blah.com." msgstr "Merci de prendre le temps de remplir le sondage. Pour toute question, veuillez communiquer avec Fulano a fulano@blah.com " ''' # Catalog not created, throws Unicode Error buf = StringIO(invalid_po) output = pofile.read_po(buf, locale='fr', abort_invalid=False) assert isinstance(output, Catalog) # Catalog not created, throws PoFileError buf = StringIO(invalid_po_2) with pytest.raises(pofile.PoFileError): pofile.read_po(buf, locale='fr', abort_invalid=True) # Catalog is created with warning, no abort buf = StringIO(invalid_po_2) output = pofile.read_po(buf, locale='fr', abort_invalid=False) assert isinstance(output, Catalog) # Catalog not created, aborted with PoFileError buf = StringIO(invalid_po_2) with pytest.raises(pofile.PoFileError): pofile.read_po(buf, locale='fr', abort_invalid=True) def test_invalid_pofile_with_abort_flag(self): parser = pofile.PoFileParser(None, abort_invalid=True) lineno = 10 line = 'Algo esta mal' msg = 'invalid file' with pytest.raises(pofile.PoFileError): parser._invalid_pofile(line, lineno, msg) class WritePoTestCase(unittest.TestCase): def test_join_locations(self): catalog = Catalog() catalog.add('foo', locations=[('main.py', 1)]) catalog.add('foo', locations=[('utils.py', 3)]) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True) assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3 msgid "foo" msgstr ""''' def test_write_po_file_with_specified_charset(self): catalog = Catalog(charset='iso-8859-1') catalog.add('foo', 'äöü', locations=[('main.py', 1)]) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=False) po_file = buf.getvalue().strip() assert b'"Content-Type: text/plain; charset=iso-8859-1\\n"' in po_file assert 'msgstr "äöü"'.encode('iso-8859-1') in po_file def test_duplicate_comments(self): catalog = Catalog() catalog.add('foo', auto_comments=['A comment']) catalog.add('foo', auto_comments=['A comment']) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True) assert buf.getvalue().strip() == b'''#. A comment msgid "foo" msgstr ""''' def test_wrap_long_lines(self): text = """Here's some text where white space and line breaks matter, and should not be removed """ catalog = Catalog() catalog.add(text, locations=[('main.py', 1)]) buf = BytesIO() pofile.write_po(buf, catalog, no_location=True, omit_header=True, width=42) assert buf.getvalue().strip() == b'''msgid "" "Here's some text where\\n" "white space and line breaks matter, and" " should\\n" "\\n" "not be removed\\n" "\\n" msgstr ""''' def test_wrap_long_lines_with_long_word(self): text = """Here's some text that includesareallylongwordthatmightbutshouldnt throw us into an infinite loop """ catalog = Catalog() catalog.add(text, locations=[('main.py', 1)]) buf = BytesIO() pofile.write_po(buf, catalog, no_location=True, omit_header=True, width=32) assert buf.getvalue().strip() == b'''msgid "" "Here's some text that\\n" "includesareallylongwordthatmightbutshouldnt" " throw us into an infinite " "loop\\n" msgstr ""''' def test_wrap_long_lines_in_header(self): """ Verify that long lines in the header comment are wrapped correctly. """ catalog = Catalog(project='AReallyReallyLongNameForAProject', revision_date=datetime(2007, 4, 1)) buf = BytesIO() pofile.write_po(buf, catalog) assert b'\n'.join(buf.getvalue().splitlines()[:7]) == b'''\ # Translations template for AReallyReallyLongNameForAProject. # Copyright (C) 2007 ORGANIZATION # This file is distributed under the same license as the # AReallyReallyLongNameForAProject project. # FIRST AUTHOR , 2007. # #, fuzzy''' def test_wrap_locations_with_hyphens(self): catalog = Catalog() catalog.add('foo', locations=[ ('doupy/templates/base/navmenu.inc.html.py', 60), ]) catalog.add('foo', locations=[ ('doupy/templates/job-offers/helpers.html', 22), ]) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True) assert buf.getvalue().strip() == b'''#: doupy/templates/base/navmenu.inc.html.py:60 #: doupy/templates/job-offers/helpers.html:22 msgid "foo" msgstr ""''' def test_no_wrap_and_width_behaviour_on_comments(self): catalog = Catalog() catalog.add("Pretty dam long message id, which must really be big " "to test this wrap behaviour, if not it won't work.", locations=[("fake.py", n) for n in range(1, 30)]) buf = BytesIO() pofile.write_po(buf, catalog, width=None, omit_header=True) assert buf.getvalue().lower() == b"""\ #: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 #: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14 #: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21 #: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28 #: fake.py:29 msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work." msgstr "" """ buf = BytesIO() pofile.write_po(buf, catalog, width=100, omit_header=True) assert buf.getvalue().lower() == b"""\ #: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10 #: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 #: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28 #: fake.py:29 msgid "" "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't" " work." msgstr "" """ def test_pot_with_translator_comments(self): catalog = Catalog() catalog.add('foo', locations=[('main.py', 1)], auto_comments=['Comment About `foo`']) catalog.add('bar', locations=[('utils.py', 3)], user_comments=['Comment About `bar` with', 'multiple lines.']) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True) assert buf.getvalue().strip() == b'''#. Comment About `foo` #: main.py:1 msgid "foo" msgstr "" # Comment About `bar` with # multiple lines. #: utils.py:3 msgid "bar" msgstr ""''' def test_po_with_obsolete_message(self): catalog = Catalog() catalog.add('foo', 'Voh', locations=[('main.py', 1)]) catalog.obsolete['bar'] = Message('bar', 'Bahr', locations=[('utils.py', 3)], user_comments=['User comment']) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True) assert buf.getvalue().strip() == b'''#: main.py:1 msgid "foo" msgstr "Voh" # User comment #~ msgid "bar" #~ msgstr "Bahr"''' def test_po_with_multiline_obsolete_message(self): catalog = Catalog() catalog.add('foo', 'Voh', locations=[('main.py', 1)]) msgid = r"""Here's a message that covers multiple lines, and should still be handled correctly. """ msgstr = r"""Here's a message that covers multiple lines, and should still be handled correctly. """ catalog.obsolete[msgid] = Message(msgid, msgstr, locations=[('utils.py', 3)]) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True) assert buf.getvalue().strip() == b'''#: main.py:1 msgid "foo" msgstr "Voh" #~ msgid "" #~ "Here's a message that covers\\n" #~ "multiple lines, and should still be handled\\n" #~ "correctly.\\n" #~ msgstr "" #~ "Here's a message that covers\\n" #~ "multiple lines, and should still be handled\\n" #~ "correctly.\\n"''' def test_po_with_obsolete_message_ignored(self): catalog = Catalog() catalog.add('foo', 'Voh', locations=[('main.py', 1)]) catalog.obsolete['bar'] = Message('bar', 'Bahr', locations=[('utils.py', 3)], user_comments=['User comment']) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True) assert buf.getvalue().strip() == b'''#: main.py:1 msgid "foo" msgstr "Voh"''' def test_po_with_previous_msgid(self): catalog = Catalog() catalog.add('foo', 'Voh', locations=[('main.py', 1)], previous_id='fo') buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True, include_previous=True) assert buf.getvalue().strip() == b'''#: main.py:1 #| msgid "fo" msgid "foo" msgstr "Voh"''' def test_po_with_previous_msgid_plural(self): catalog = Catalog() catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)], previous_id=('fo', 'fos')) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True, include_previous=True) assert buf.getvalue().strip() == b'''#: main.py:1 #| msgid "fo" #| msgid_plural "fos" msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Voeh"''' def test_sorted_po(self): catalog = Catalog() catalog.add('bar', locations=[('utils.py', 3)], user_comments=['Comment About `bar` with', 'multiple lines.']) catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)]) buf = BytesIO() pofile.write_po(buf, catalog, sort_output=True) value = buf.getvalue().strip() assert b'''\ # Comment About `bar` with # multiple lines. #: utils.py:3 msgid "bar" msgstr "" #: main.py:1 msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Voeh"''' in value assert value.find(b'msgid ""') < value.find(b'msgid "bar"') < value.find(b'msgid "foo"') def test_sorted_po_context(self): catalog = Catalog() catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)], context='there') catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)]) catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)], context='here') buf = BytesIO() pofile.write_po(buf, catalog, sort_output=True) value = buf.getvalue().strip() # We expect the foo without ctx, followed by "here" foo and "there" foo assert b'''\ #: main.py:1 msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Voeh" #: main.py:1 msgctxt "here" msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Voeh" #: main.py:1 msgctxt "there" msgid "foo" msgid_plural "foos" msgstr[0] "Voh" msgstr[1] "Voeh"''' in value def test_file_sorted_po(self): catalog = Catalog() catalog.add('bar', locations=[('utils.py', 3)]) catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)]) buf = BytesIO() pofile.write_po(buf, catalog, sort_by_file=True) value = buf.getvalue().strip() assert value.find(b'main.py') < value.find(b'utils.py') def test_file_with_no_lineno(self): catalog = Catalog() catalog.add('bar', locations=[('utils.py', None)], user_comments=['Comment About `bar` with', 'multiple lines.']) buf = BytesIO() pofile.write_po(buf, catalog, sort_output=True) value = buf.getvalue().strip() assert b'''\ # Comment About `bar` with # multiple lines. #: utils.py msgid "bar" msgstr ""''' in value def test_silent_location_fallback(self): buf = BytesIO(b'''\ #: broken_file.py msgid "missing line number" msgstr "" #: broken_file.py:broken_line_number msgid "broken line number" msgstr ""''') catalog = pofile.read_po(buf) assert catalog['missing line number'].locations == [('broken_file.py', None)] assert catalog['broken line number'].locations == [] def test_include_lineno(self): catalog = Catalog() catalog.add('foo', locations=[('main.py', 1)]) catalog.add('foo', locations=[('utils.py', 3)]) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True, include_lineno=True) assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3 msgid "foo" msgstr ""''' def test_no_include_lineno(self): catalog = Catalog() catalog.add('foo', locations=[('main.py', 1)]) catalog.add('foo', locations=[('main.py', 2)]) catalog.add('foo', locations=[('utils.py', 3)]) buf = BytesIO() pofile.write_po(buf, catalog, omit_header=True, include_lineno=False) assert buf.getvalue().strip() == b'''#: main.py utils.py msgid "foo" msgstr ""''' class PofileFunctionsTestCase(unittest.TestCase): def test_unescape(self): escaped = '"Say:\\n \\"hello, world!\\"\\n"' unescaped = 'Say:\n "hello, world!"\n' assert unescaped != escaped assert unescaped == pofile.unescape(escaped) def test_unescape_of_quoted_newline(self): # regression test for #198 assert pofile.unescape(r'"\\n"') == '\\n' def test_denormalize_on_msgstr_without_empty_first_line(self): # handle irregular multi-line msgstr (no "" as first line) # gracefully (#171) msgstr = '"multi-line\\n"\n" translation"' expected_denormalized = 'multi-line\n translation' assert expected_denormalized == pofile.denormalize(msgstr) assert expected_denormalized == pofile.denormalize(f'""\n{msgstr}') def test_unknown_language_roundtrip(): buf = StringIO(r''' msgid "" msgstr "" "Language: sr_SP\n"''') catalog = pofile.read_po(buf) assert catalog.locale_identifier == 'sr_SP' assert not catalog.locale buf = BytesIO() pofile.write_po(buf, catalog) assert 'sr_SP' in buf.getvalue().decode() def test_unknown_language_write(): catalog = Catalog(locale='sr_SP') assert catalog.locale_identifier == 'sr_SP' assert not catalog.locale buf = BytesIO() pofile.write_po(buf, catalog) assert 'sr_SP' in buf.getvalue().decode() babel-2.14.0/tests/messages/test_js_extract.py0000644000175000017500000001350114536056757020743 0ustar nileshnileshfrom io import BytesIO import pytest from babel.messages import extract def test_simple_extract(): buf = BytesIO(b"""\ msg1 = _('simple') msg2 = gettext('simple') msg3 = ngettext('s', 'p', 42) """) messages = \ list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, [], {})) assert messages == [(1, 'simple', [], None), (2, 'simple', [], None), (3, ('s', 'p'), [], None)] def test_various_calls(): buf = BytesIO(b"""\ msg1 = _(i18n_arg.replace(/"/, '"')) msg2 = ungettext(i18n_arg.replace(/"/, '"'), multi_arg.replace(/"/, '"'), 2) msg3 = ungettext("Babel", multi_arg.replace(/"/, '"'), 2) msg4 = ungettext(i18n_arg.replace(/"/, '"'), "Babels", 2) msg5 = ungettext('bunny', 'bunnies', parseInt(Math.random() * 2 + 1)) msg6 = ungettext(arg0, 'bunnies', rparseInt(Math.random() * 2 + 1)) msg7 = _(hello.there) msg8 = gettext('Rabbit') msg9 = dgettext('wiki', model.addPage()) msg10 = dngettext(domain, 'Page', 'Pages', 3) """) messages = \ list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, [], {})) assert messages == [ (5, ('bunny', 'bunnies'), [], None), (8, 'Rabbit', [], None), (10, ('Page', 'Pages'), [], None), ] def test_message_with_line_comment(): buf = BytesIO("""\ // NOTE: hello msg = _('Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][3] == ['NOTE: hello'] def test_message_with_multiline_comment(): buf = BytesIO("""\ /* NOTE: hello and bonjour and servus */ msg = _('Bonjour à tous') """.encode('utf-8')) messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Bonjour à tous' assert messages[0][3] == ['NOTE: hello', 'and bonjour', ' and servus'] def test_ignore_function_definitions(): buf = BytesIO(b"""\ function gettext(value) { return translations[language][value] || value; }""") messages = list(extract.extract_javascript(buf, ('gettext',), [], {})) assert not messages def test_misplaced_comments(): buf = BytesIO(b"""\ /* NOTE: this won't show up */ foo() /* NOTE: this will */ msg = _('Something') // NOTE: this will show up // too. msg = _('Something else') // NOTE: but this won't bar() _('no comment here') """) messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) assert messages[0][2] == 'Something' assert messages[0][3] == ['NOTE: this will'] assert messages[1][2] == 'Something else' assert messages[1][3] == ['NOTE: this will show up', 'too.'] assert messages[2][2] == 'no comment here' assert messages[2][3] == [] JSX_SOURCE = b""" class Foo { render() { const value = gettext("hello"); return ( ); } """ EXPECTED_JSX_MESSAGES = ["hello", "String1", "String 2", "String 3", "String 4", "String 5"] @pytest.mark.parametrize("jsx_enabled", (False, True)) def test_jsx_extraction(jsx_enabled): buf = BytesIO(JSX_SOURCE) messages = [m[2] for m in extract.extract_javascript(buf, ('_', 'gettext'), [], {"jsx": jsx_enabled})] if jsx_enabled: assert messages == EXPECTED_JSX_MESSAGES else: assert messages != EXPECTED_JSX_MESSAGES def test_dotted_keyword_extract(): buf = BytesIO(b"msg1 = com.corporate.i18n.formatMessage('Insert coin to continue')") messages = list( extract.extract('javascript', buf, {"com.corporate.i18n.formatMessage": None}, [], {}), ) assert messages == [(1, 'Insert coin to continue', [], None)] def test_template_string_standard_usage(): buf = BytesIO(b"msg1 = gettext(`Very template, wow`)") messages = list( extract.extract('javascript', buf, {"gettext": None}, [], {}), ) assert messages == [(1, 'Very template, wow', [], None)] def test_template_string_tag_usage(): buf = BytesIO(b"function() { if(foo) msg1 = i18n`Tag template, wow`; }") messages = list( extract.extract('javascript', buf, {"i18n": None}, [], {}), ) assert messages == [(1, 'Tag template, wow', [], None)] def test_inside_template_string(): buf = BytesIO(b"const msg = `${gettext('Hello')} ${user.name}`") messages = list( extract.extract('javascript', buf, {"gettext": None}, [], {'parse_template_string': True}), ) assert messages == [(1, 'Hello', [], None)] def test_inside_template_string_with_linebreaks(): buf = BytesIO(b"""\ const userName = gettext('Username') const msg = `${ gettext('Hello') } ${userName} ${ gettext('Are you having a nice day?') }` const msg2 = `${ gettext('Howdy') } ${userName} ${ gettext('Are you doing ok?') }` """) messages = list( extract.extract('javascript', buf, {"gettext": None}, [], {'parse_template_string': True}), ) assert messages == [(1, 'Username', [], None), (3, 'Hello', [], None), (5, 'Are you having a nice day?', [], None), (8, 'Howdy', [], None), (10, 'Are you doing ok?', [], None)] def test_inside_nested_template_string(): buf = BytesIO(b"const msg = `${gettext('Greetings!')} ${ evening ? `${user.name}: ${gettext('This is a lovely evening.')}` : `${gettext('The day is really nice!')} ${user.name}`}`") messages = list( extract.extract('javascript', buf, {"gettext": None}, [], {'parse_template_string': True}), ) assert messages == [(1, 'Greetings!', [], None), (1, 'This is a lovely evening.', [], None), (1, 'The day is really nice!', [], None)] babel-2.14.0/tests/messages/consts.py0000644000175000017500000000055114536056757017050 0ustar nileshnileshimport os TEST_PROJECT_DISTRIBUTION_DATA = { "name": "TestProject", "version": "0.1", "packages": ["project"], } this_dir = os.path.abspath(os.path.dirname(__file__)) data_dir = os.path.join(this_dir, 'data') project_dir = os.path.join(data_dir, 'project') i18n_dir = os.path.join(project_dir, 'i18n') pot_file = os.path.join(i18n_dir, 'temp.pot') babel-2.14.0/tests/messages/test_catalog.py0000644000175000017500000004723314536056757020220 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import copy import datetime import unittest from io import StringIO from babel.dates import UTC, format_datetime from babel.messages import catalog, pofile from babel.util import FixedOffsetTimezone class MessageTestCase(unittest.TestCase): def test_python_format(self): assert catalog.PYTHON_FORMAT.search('foo %d bar') assert catalog.PYTHON_FORMAT.search('foo %s bar') assert catalog.PYTHON_FORMAT.search('foo %r bar') assert catalog.PYTHON_FORMAT.search('foo %(name).1f') assert catalog.PYTHON_FORMAT.search('foo %(name)3.3f') assert catalog.PYTHON_FORMAT.search('foo %(name)3f') assert catalog.PYTHON_FORMAT.search('foo %(name)06d') assert catalog.PYTHON_FORMAT.search('foo %(name)Li') assert catalog.PYTHON_FORMAT.search('foo %(name)#d') assert catalog.PYTHON_FORMAT.search('foo %(name)-4.4hs') assert catalog.PYTHON_FORMAT.search('foo %(name)*.3f') assert catalog.PYTHON_FORMAT.search('foo %(name).*f') assert catalog.PYTHON_FORMAT.search('foo %(name)3.*f') assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f') assert catalog.PYTHON_FORMAT.search('foo %()s') def test_translator_comments(self): mess = catalog.Message('foo', user_comments=['Comment About `foo`']) assert mess.user_comments == ['Comment About `foo`'] mess = catalog.Message('foo', auto_comments=['Comment 1 About `foo`', 'Comment 2 About `foo`']) assert mess.auto_comments == ['Comment 1 About `foo`', 'Comment 2 About `foo`'] def test_clone_message_object(self): msg = catalog.Message('foo', locations=[('foo.py', 42)]) clone = msg.clone() clone.locations.append(('bar.py', 42)) assert msg.locations == [('foo.py', 42)] msg.flags.add('fuzzy') assert not clone.fuzzy and msg.fuzzy class CatalogTestCase(unittest.TestCase): def test_add_returns_message_instance(self): cat = catalog.Catalog() message = cat.add('foo') assert message.id == 'foo' def test_two_messages_with_same_singular(self): cat = catalog.Catalog() cat.add('foo') cat.add(('foo', 'foos')) assert len(cat) == 1 def test_duplicate_auto_comment(self): cat = catalog.Catalog() cat.add('foo', auto_comments=['A comment']) cat.add('foo', auto_comments=['A comment', 'Another comment']) assert cat['foo'].auto_comments == ['A comment', 'Another comment'] def test_duplicate_user_comment(self): cat = catalog.Catalog() cat.add('foo', user_comments=['A comment']) cat.add('foo', user_comments=['A comment', 'Another comment']) assert cat['foo'].user_comments == ['A comment', 'Another comment'] def test_duplicate_location(self): cat = catalog.Catalog() cat.add('foo', locations=[('foo.py', 1)]) cat.add('foo', locations=[('foo.py', 1)]) assert cat['foo'].locations == [('foo.py', 1)] def test_update_message_changed_to_plural(self): cat = catalog.Catalog() cat.add('foo', 'Voh') tmpl = catalog.Catalog() tmpl.add(('foo', 'foos')) cat.update(tmpl) assert cat['foo'].string == ('Voh', '') assert cat['foo'].fuzzy def test_update_message_changed_to_simple(self): cat = catalog.Catalog() cat.add('foo' 'foos', ('Voh', 'Vöhs')) tmpl = catalog.Catalog() tmpl.add('foo') cat.update(tmpl) assert cat['foo'].string == 'Voh' assert cat['foo'].fuzzy def test_update_message_updates_comments(self): cat = catalog.Catalog() cat['foo'] = catalog.Message('foo', locations=[('main.py', 5)]) assert cat['foo'].auto_comments == [] assert cat['foo'].user_comments == [] # Update cat[u'foo'] with a new location and a comment cat['foo'] = catalog.Message('foo', locations=[('main.py', 7)], user_comments=['Foo Bar comment 1']) assert cat['foo'].user_comments == ['Foo Bar comment 1'] # now add yet another location with another comment cat['foo'] = catalog.Message('foo', locations=[('main.py', 9)], auto_comments=['Foo Bar comment 2']) assert cat['foo'].auto_comments == ['Foo Bar comment 2'] def test_update_fuzzy_matching_with_case_change(self): cat = catalog.Catalog() cat.add('FOO', 'Voh') cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('foo') cat.update(tmpl) assert len(cat.obsolete) == 1 assert 'FOO' not in cat assert cat['foo'].string == 'Voh' assert cat['foo'].fuzzy is True def test_update_fuzzy_matching_with_char_change(self): cat = catalog.Catalog() cat.add('fo', 'Voh') cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('foo') cat.update(tmpl) assert len(cat.obsolete) == 1 assert 'fo' not in cat assert cat['foo'].string == 'Voh' assert cat['foo'].fuzzy is True def test_update_fuzzy_matching_no_msgstr(self): cat = catalog.Catalog() cat.add('fo', '') tmpl = catalog.Catalog() tmpl.add('fo') tmpl.add('foo') cat.update(tmpl) assert 'fo' in cat assert 'foo' in cat assert cat['fo'].string == '' assert cat['fo'].fuzzy is False assert cat['foo'].string is None assert cat['foo'].fuzzy is False def test_update_fuzzy_matching_with_new_context(self): cat = catalog.Catalog() cat.add('foo', 'Voh') cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('Foo', context='Menu') cat.update(tmpl) assert len(cat.obsolete) == 1 assert 'foo' not in cat message = cat.get('Foo', 'Menu') assert message.string == 'Voh' assert message.fuzzy is True assert message.context == 'Menu' def test_update_fuzzy_matching_with_changed_context(self): cat = catalog.Catalog() cat.add('foo', 'Voh', context='Menu|File') cat.add('bar', 'Bahr', context='Menu|File') tmpl = catalog.Catalog() tmpl.add('Foo', context='Menu|Edit') cat.update(tmpl) assert len(cat.obsolete) == 1 assert cat.get('Foo', 'Menu|File') is None message = cat.get('Foo', 'Menu|Edit') assert message.string == 'Voh' assert message.fuzzy is True assert message.context == 'Menu|Edit' def test_update_fuzzy_matching_no_cascading(self): cat = catalog.Catalog() cat.add('fo', 'Voh') cat.add('foo', 'Vohe') tmpl = catalog.Catalog() tmpl.add('fo') tmpl.add('foo') tmpl.add('fooo') cat.update(tmpl) assert 'fo' in cat assert 'foo' in cat assert cat['fo'].string == 'Voh' assert cat['fo'].fuzzy is False assert cat['foo'].string == 'Vohe' assert cat['foo'].fuzzy is False assert cat['fooo'].string == 'Vohe' assert cat['fooo'].fuzzy is True def test_update_fuzzy_matching_long_string(self): lipsum = "\ Lorem Ipsum is simply dummy text of the printing and typesetting \ industry. Lorem Ipsum has been the industry's standard dummy text ever \ since the 1500s, when an unknown printer took a galley of type and \ scrambled it to make a type specimen book. It has survived not only \ five centuries, but also the leap into electronic typesetting, \ remaining essentially unchanged. It was popularised in the 1960s with \ the release of Letraset sheets containing Lorem Ipsum passages, and \ more recently with desktop publishing software like Aldus PageMaker \ including versions of Lorem Ipsum." cat = catalog.Catalog() cat.add("ZZZZZZ " + lipsum, "foo") tmpl = catalog.Catalog() tmpl.add(lipsum + " ZZZZZZ") cat.update(tmpl) assert cat[lipsum + " ZZZZZZ"].fuzzy is True assert len(cat.obsolete) == 0 def test_update_without_fuzzy_matching(self): cat = catalog.Catalog() cat.add('fo', 'Voh') cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('foo') cat.update(tmpl, no_fuzzy_matching=True) assert len(cat.obsolete) == 2 def test_fuzzy_matching_regarding_plurals(self): cat = catalog.Catalog() cat.add(('foo', 'foh'), ('foo', 'foh')) ru = copy.copy(cat) ru.locale = 'ru_RU' ru.update(cat) assert ru['foo'].fuzzy is True ru = copy.copy(cat) ru.locale = 'ru_RU' ru['foo'].string = ('foh', 'fohh', 'fohhh') ru.update(cat) assert ru['foo'].fuzzy is False def test_update_no_template_mutation(self): tmpl = catalog.Catalog() tmpl.add('foo') cat1 = catalog.Catalog() cat1.add('foo', 'Voh') cat1.update(tmpl) cat2 = catalog.Catalog() cat2.update(tmpl) assert cat2['foo'].string is None assert cat2['foo'].fuzzy is False def test_update_po_updates_pot_creation_date(self): template = catalog.Catalog() localized_catalog = copy.deepcopy(template) localized_catalog.locale = 'de_DE' assert template.mime_headers != localized_catalog.mime_headers assert template.creation_date == localized_catalog.creation_date template.creation_date = datetime.datetime.now() - \ datetime.timedelta(minutes=5) localized_catalog.update(template) assert template.creation_date == localized_catalog.creation_date def test_update_po_ignores_pot_creation_date(self): template = catalog.Catalog() localized_catalog = copy.deepcopy(template) localized_catalog.locale = 'de_DE' assert template.mime_headers != localized_catalog.mime_headers assert template.creation_date == localized_catalog.creation_date template.creation_date = datetime.datetime.now() - \ datetime.timedelta(minutes=5) localized_catalog.update(template, update_creation_date=False) assert template.creation_date != localized_catalog.creation_date def test_update_po_keeps_po_revision_date(self): template = catalog.Catalog() localized_catalog = copy.deepcopy(template) localized_catalog.locale = 'de_DE' fake_rev_date = datetime.datetime.now() - datetime.timedelta(days=5) localized_catalog.revision_date = fake_rev_date assert template.mime_headers != localized_catalog.mime_headers assert template.creation_date == localized_catalog.creation_date template.creation_date = datetime.datetime.now() - \ datetime.timedelta(minutes=5) localized_catalog.update(template) assert localized_catalog.revision_date == fake_rev_date def test_stores_datetime_correctly(self): localized = catalog.Catalog() localized.locale = 'de_DE' localized[''] = catalog.Message('', "POT-Creation-Date: 2009-03-09 15:47-0700\n" + "PO-Revision-Date: 2009-03-09 15:47-0700\n") for key, value in localized.mime_headers: if key in ('POT-Creation-Date', 'PO-Revision-Date'): assert value == '2009-03-09 15:47-0700' def test_mime_headers_contain_same_information_as_attributes(self): cat = catalog.Catalog() cat[''] = catalog.Message('', "Last-Translator: Foo Bar \n" + "Language-Team: de \n" + "POT-Creation-Date: 2009-03-01 11:20+0200\n" + "PO-Revision-Date: 2009-03-09 15:47-0700\n") assert cat.locale is None mime_headers = dict(cat.mime_headers) assert cat.last_translator == 'Foo Bar ' assert mime_headers['Last-Translator'] == 'Foo Bar ' assert cat.language_team == 'de ' assert mime_headers['Language-Team'] == 'de ' dt = datetime.datetime(2009, 3, 9, 15, 47, tzinfo=FixedOffsetTimezone(-7 * 60)) assert cat.revision_date == dt formatted_dt = format_datetime(dt, 'yyyy-MM-dd HH:mmZ', locale='en') assert mime_headers['PO-Revision-Date'] == formatted_dt def test_message_fuzzy(): assert not catalog.Message('foo').fuzzy msg = catalog.Message('foo', 'foo', flags=['fuzzy']) assert msg.fuzzy assert msg.id == 'foo' def test_message_pluralizable(): assert not catalog.Message('foo').pluralizable assert catalog.Message(('foo', 'bar')).pluralizable def test_message_python_format(): assert catalog.Message('foo %(name)s bar').python_format assert catalog.Message(('foo %(name)s', 'foo %(name)s')).python_format def test_catalog(): cat = catalog.Catalog(project='Foobar', version='1.0', copyright_holder='Foo Company') assert cat.header_comment == ( '# Translations template for Foobar.\n' '# Copyright (C) %(year)d Foo Company\n' '# This file is distributed under the same ' 'license as the Foobar project.\n' '# FIRST AUTHOR , %(year)d.\n' '#') % {'year': datetime.date.today().year} cat = catalog.Catalog(project='Foobar', version='1.0', copyright_holder='Foo Company') cat.header_comment = ( '# The POT for my really cool PROJECT project.\n' '# Copyright (C) 1990-2003 ORGANIZATION\n' '# This file is distributed under the same license as the PROJECT\n' '# project.\n' '#\n') assert cat.header_comment == ( '# The POT for my really cool Foobar project.\n' '# Copyright (C) 1990-2003 Foo Company\n' '# This file is distributed under the same license as the Foobar\n' '# project.\n' '#\n') def test_catalog_mime_headers(): created = datetime.datetime(1990, 4, 1, 15, 30, tzinfo=UTC) cat = catalog.Catalog(project='Foobar', version='1.0', creation_date=created) assert cat.mime_headers == [ ('Project-Id-Version', 'Foobar 1.0'), ('Report-Msgid-Bugs-To', 'EMAIL@ADDRESS'), ('POT-Creation-Date', '1990-04-01 15:30+0000'), ('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE'), ('Last-Translator', 'FULL NAME '), ('Language-Team', 'LANGUAGE '), ('MIME-Version', '1.0'), ('Content-Type', 'text/plain; charset=utf-8'), ('Content-Transfer-Encoding', '8bit'), ('Generated-By', f'Babel {catalog.VERSION}\n'), ] def test_catalog_mime_headers_set_locale(): created = datetime.datetime(1990, 4, 1, 15, 30, tzinfo=UTC) revised = datetime.datetime(1990, 8, 3, 12, 0, tzinfo=UTC) cat = catalog.Catalog(locale='de_DE', project='Foobar', version='1.0', creation_date=created, revision_date=revised, last_translator='John Doe ', language_team='de_DE ') assert cat.mime_headers == [ ('Project-Id-Version', 'Foobar 1.0'), ('Report-Msgid-Bugs-To', 'EMAIL@ADDRESS'), ('POT-Creation-Date', '1990-04-01 15:30+0000'), ('PO-Revision-Date', '1990-08-03 12:00+0000'), ('Last-Translator', 'John Doe '), ('Language', 'de_DE'), ('Language-Team', 'de_DE '), ('Plural-Forms', 'nplurals=2; plural=(n != 1);'), ('MIME-Version', '1.0'), ('Content-Type', 'text/plain; charset=utf-8'), ('Content-Transfer-Encoding', '8bit'), ('Generated-By', f'Babel {catalog.VERSION}\n'), ] def test_catalog_num_plurals(): assert catalog.Catalog(locale='en').num_plurals == 2 assert catalog.Catalog(locale='ga').num_plurals == 5 def test_catalog_plural_expr(): assert catalog.Catalog(locale='en').plural_expr == '(n != 1)' assert (catalog.Catalog(locale='ga').plural_expr == '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)') def test_catalog_plural_forms(): assert (catalog.Catalog(locale='en').plural_forms == 'nplurals=2; plural=(n != 1);') assert (catalog.Catalog(locale='pt_BR').plural_forms == 'nplurals=2; plural=(n > 1);') def test_catalog_setitem(): cat = catalog.Catalog() cat['foo'] = catalog.Message('foo') assert cat['foo'].id == 'foo' cat = catalog.Catalog() cat['foo'] = catalog.Message('foo', locations=[('main.py', 1)]) assert cat['foo'].locations == [('main.py', 1)] cat['foo'] = catalog.Message('foo', locations=[('utils.py', 5)]) assert cat['foo'].locations == [('main.py', 1), ('utils.py', 5)] def test_catalog_add(): cat = catalog.Catalog() foo = cat.add('foo') assert foo.id == 'foo' assert cat['foo'] is foo def test_catalog_update(): template = catalog.Catalog(header_comment="# A Custom Header") template.add('green', locations=[('main.py', 99)]) template.add('blue', locations=[('main.py', 100)]) template.add(('salad', 'salads'), locations=[('util.py', 42)]) cat = catalog.Catalog(locale='de_DE') cat.add('blue', 'blau', locations=[('main.py', 98)]) cat.add('head', 'Kopf', locations=[('util.py', 33)]) cat.add(('salad', 'salads'), ('Salat', 'Salate'), locations=[('util.py', 38)]) cat.update(template) assert len(cat) == 3 msg1 = cat['green'] assert not msg1.string assert msg1.locations == [('main.py', 99)] msg2 = cat['blue'] assert msg2.string == 'blau' assert msg2.locations == [('main.py', 100)] msg3 = cat['salad'] assert msg3.string == ('Salat', 'Salate') assert msg3.locations == [('util.py', 42)] assert 'head' not in cat assert list(cat.obsolete.values())[0].id == 'head' cat.update(template, update_header_comment=True) assert cat.header_comment == template.header_comment # Header comment also gets updated def test_datetime_parsing(): val1 = catalog._parse_datetime_header('2006-06-28 23:24+0200') assert val1.year == 2006 assert val1.month == 6 assert val1.day == 28 assert val1.tzinfo.zone == 'Etc/GMT+120' val2 = catalog._parse_datetime_header('2006-06-28 23:24') assert val2.year == 2006 assert val2.month == 6 assert val2.day == 28 assert val2.tzinfo is None def test_update_catalog_comments(): # Based on https://web.archive.org/web/20100710131029/http://babel.edgewall.org/attachment/ticket/163/cat-update-comments.py catalog = pofile.read_po(StringIO(''' # A user comment #. An auto comment #: main.py:1 #, fuzzy, python-format msgid "foo %(name)s" msgstr "foo %(name)s" ''')) assert all(message.user_comments and message.auto_comments for message in catalog if message.id) # NOTE: in the POT file, there are no comments template = pofile.read_po(StringIO(''' #: main.py:1 #, fuzzy, python-format msgid "bar %(name)s" msgstr "" ''')) catalog.update(template) # Auto comments will be obliterated here assert all(message.user_comments for message in catalog if message.id) babel-2.14.0/tests/test_date_intervals.py0000644000175000017500000000370114536056757017773 0ustar nileshnileshimport datetime from babel import dates from babel.util import UTC TEST_DT = datetime.datetime(2016, 1, 8, 11, 46, 15) TEST_TIME = TEST_DT.time() TEST_DATE = TEST_DT.date() def test_format_interval_same_instant_1(): assert dates.format_interval(TEST_DT, TEST_DT, "yMMMd", fuzzy=False, locale="fi") == "8. tammik. 2016" def test_format_interval_same_instant_2(): assert dates.format_interval(TEST_DT, TEST_DT, "xxx", fuzzy=False, locale="fi") == "8.1.2016 11.46.15" def test_format_interval_same_instant_3(): assert dates.format_interval(TEST_TIME, TEST_TIME, "xxx", fuzzy=False, locale="fi") == "11.46.15" def test_format_interval_same_instant_4(): assert dates.format_interval(TEST_DATE, TEST_DATE, "xxx", fuzzy=False, locale="fi") == "8.1.2016" def test_format_interval_no_difference(): t1 = TEST_DT t2 = t1 + datetime.timedelta(minutes=8) assert dates.format_interval(t1, t2, "yMd", fuzzy=False, locale="fi") == "8.1.2016" def test_format_interval_in_tz(timezone_getter): t1 = TEST_DT.replace(tzinfo=UTC) t2 = t1 + datetime.timedelta(minutes=18) hki_tz = timezone_getter("Europe/Helsinki") assert dates.format_interval(t1, t2, "Hmv", tzinfo=hki_tz, locale="fi") == "13.46\u201314.04 aikavyöhyke: Suomi" def test_format_interval_12_hour(): t2 = TEST_DT t1 = t2 - datetime.timedelta(hours=1) assert dates.format_interval(t1, t2, "hm", locale="en") == "10:46\u2009\u2013\u200911:46\u202fAM" def test_format_interval_invalid_skeleton(): t1 = TEST_DATE t2 = TEST_DATE + datetime.timedelta(days=1) assert dates.format_interval(t1, t2, "mumumu", fuzzy=False, locale="fi") == "8.1.2016\u20139.1.2016" assert dates.format_interval(t1, t2, fuzzy=False, locale="fi") == "8.1.2016\u20139.1.2016" def test_issue_825(): assert dates.format_timedelta( datetime.timedelta(hours=1), granularity='hour', threshold=100, format='short', locale='pt', ) == '1 h' babel-2.14.0/tests/conftest.py0000644000175000017500000000230314536056757015552 0ustar nileshnileshimport os import pytest try: import zoneinfo except ModuleNotFoundError: try: from backports import zoneinfo except ImportError: zoneinfo = None try: import pytz except ModuleNotFoundError: pytz = None @pytest.fixture def os_environ(monkeypatch): mock_environ = dict(os.environ) monkeypatch.setattr(os, 'environ', mock_environ) return mock_environ def pytest_generate_tests(metafunc): if hasattr(metafunc.function, "pytestmark"): for mark in metafunc.function.pytestmark: if mark.name == "all_locales": from babel.localedata import locale_identifiers metafunc.parametrize("locale", list(locale_identifiers())) break @pytest.fixture(params=["pytz.timezone", "zoneinfo.ZoneInfo"], scope="package") def timezone_getter(request): if request.param == "pytz.timezone": if pytz: return pytz.timezone else: pytest.skip("pytz not available") elif request.param == "zoneinfo.ZoneInfo": if zoneinfo: return zoneinfo.ZoneInfo else: pytest.skip("zoneinfo not available") else: raise NotImplementedError babel-2.14.0/tests/test_smoke.py0000644000175000017500000000474714536056757016120 0ustar nileshnilesh""" These tests do not verify any results and should not be run when looking at improving test coverage. They just verify that basic operations don't fail due to odd corner cases on any locale that we ship. """ import datetime import decimal import pytest from babel import Locale, dates, numbers, units NUMBERS = ( decimal.Decimal("-33.76"), # Negative Decimal decimal.Decimal("13.37"), # Positive Decimal 1.2 - 1.0, # Inaccurate float 10, # Plain old integer 0, # Zero ) @pytest.mark.all_locales def test_smoke_dates(locale): locale = Locale.parse(locale) instant = datetime.datetime.now() for width in ("full", "long", "medium", "short"): assert dates.format_date(instant, format=width, locale=locale) assert dates.format_datetime(instant, format=width, locale=locale) assert dates.format_time(instant, format=width, locale=locale) # Interval test past = instant - datetime.timedelta(hours=23) assert dates.format_interval(past, instant, locale=locale) # Duration test - at the time of writing, all locales seem to have `short` width, # so let's test that. duration = instant - instant.replace(hour=0, minute=0, second=0) for granularity in ('second', 'minute', 'hour', 'day'): assert dates.format_timedelta(duration, granularity=granularity, format="short", locale=locale) @pytest.mark.all_locales def test_smoke_numbers(locale): locale = Locale.parse(locale) for number in NUMBERS: assert numbers.format_decimal(number, locale=locale) assert numbers.format_decimal(number, locale=locale, numbering_system="default") assert numbers.format_currency(number, "EUR", locale=locale) assert numbers.format_currency(number, "EUR", locale=locale, numbering_system="default") assert numbers.format_scientific(number, locale=locale) assert numbers.format_scientific(number, locale=locale, numbering_system="default") assert numbers.format_percent(number / 100, locale=locale) assert numbers.format_percent(number / 100, locale=locale, numbering_system="default") @pytest.mark.all_locales def test_smoke_units(locale): locale = Locale.parse(locale) for unit in ('length-meter', 'mass-kilogram', 'energy-calorie', 'volume-liter'): for number in NUMBERS: assert units.format_unit(number, measurement_unit=unit, locale=locale) assert units.format_unit(number, measurement_unit=unit, locale=locale, numbering_system="default") babel-2.14.0/tests/__init__.py0000644000175000017500000000000014536056757015454 0ustar nileshnileshbabel-2.14.0/tests/test_core.py0000644000175000017500000003223114536056757015717 0ustar nileshnilesh# # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import pytest from babel import core from babel.core import Locale, default_locale def test_locale_provides_access_to_cldr_locale_data(): locale = Locale('en', 'US') assert locale.display_name == 'English (United States)' assert locale.number_symbols["latn"]['decimal'] == '.' def test_locale_repr(): assert repr(Locale('en', 'US')) == "Locale('en', territory='US')" assert (repr(Locale('de', 'DE')) == "Locale('de', territory='DE')") assert (repr(Locale('zh', 'CN', script='Hans')) == "Locale('zh', territory='CN', script='Hans')") def test_locale_comparison(): en_US = Locale('en', 'US') en_US_2 = Locale('en', 'US') fi_FI = Locale('fi', 'FI') bad_en_US = Locale('en_US') assert en_US == en_US assert en_US == en_US_2 assert en_US != fi_FI assert not (en_US != en_US_2) assert en_US is not None assert en_US != bad_en_US assert fi_FI != bad_en_US def test_can_return_default_locale(os_environ): os_environ['LC_MESSAGES'] = 'fr_FR.UTF-8' assert Locale('fr', 'FR') == Locale.default('LC_MESSAGES') def test_ignore_invalid_locales_in_lc_ctype(os_environ): # This is a regression test specifically for a bad LC_CTYPE setting on # MacOS X 10.6 (#200) os_environ['LC_CTYPE'] = 'UTF-8' # must not throw an exception default_locale('LC_CTYPE') def test_get_global(): assert core.get_global('zone_aliases')['GMT'] == 'Etc/GMT' assert core.get_global('zone_aliases')['UTC'] == 'Etc/UTC' assert core.get_global('zone_territories')['Europe/Berlin'] == 'DE' def test_hash(): locale_a = Locale('en', 'US') locale_b = Locale('en', 'US') locale_c = Locale('fi', 'FI') assert hash(locale_a) == hash(locale_b) assert hash(locale_a) != hash(locale_c) class TestLocaleClass: def test_attributes(self): locale = Locale('en', 'US') assert locale.language == 'en' assert locale.territory == 'US' def test_default(self, os_environ): for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']: os_environ[name] = '' os_environ['LANG'] = 'fr_FR.UTF-8' default = Locale.default('LC_MESSAGES') assert (default.language, default.territory) == ('fr', 'FR') def test_negotiate(self): de_DE = Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) assert (de_DE.language, de_DE.territory) == ('de', 'DE') de = Locale.negotiate(['de_DE', 'en_US'], ['en', 'de']) assert (de.language, de.territory) == ('de', None) nothing = Locale.negotiate(['de_DE', 'de'], ['en_US']) assert nothing is None def test_negotiate_custom_separator(self): de_DE = Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-') assert (de_DE.language, de_DE.territory) == ('de', 'DE') def test_parse(self): locale = Locale.parse('de-DE', sep='-') assert locale.display_name == 'Deutsch (Deutschland)' de_DE = Locale.parse(locale) assert (de_DE.language, de_DE.territory) == ('de', 'DE') def test_parse_likely_subtags(self): locale = Locale.parse('zh-TW', sep='-') assert locale.language == 'zh' assert locale.territory == 'TW' assert locale.script == 'Hant' locale = Locale.parse('zh_CN') assert locale.language == 'zh' assert locale.territory == 'CN' assert locale.script == 'Hans' locale = Locale.parse('zh_SG') assert locale.language == 'zh' assert locale.territory == 'SG' assert locale.script == 'Hans' locale = Locale.parse('und_AT') assert locale.language == 'de' assert locale.territory == 'AT' locale = Locale.parse('und_UK') assert locale.language == 'en' assert locale.territory == 'GB' assert locale.script is None def test_get_display_name(self): zh_CN = Locale('zh', 'CN', script='Hans') assert zh_CN.get_display_name('en') == 'Chinese (Simplified, China)' def test_display_name_property(self): assert Locale('en').display_name == 'English' assert Locale('en', 'US').display_name == 'English (United States)' assert Locale('sv').display_name == 'svenska' def test_english_name_property(self): assert Locale('de').english_name == 'German' assert Locale('de', 'DE').english_name == 'German (Germany)' def test_languages_property(self): assert Locale('de', 'DE').languages['ja'] == 'Japanisch' def test_scripts_property(self): assert Locale('en', 'US').scripts['Hira'] == 'Hiragana' def test_territories_property(self): assert Locale('es', 'CO').territories['DE'] == 'Alemania' def test_variants_property(self): assert (Locale('de', 'DE').variants['1901'] == 'Alte deutsche Rechtschreibung') def test_currencies_property(self): assert Locale('en').currencies['COP'] == 'Colombian Peso' assert Locale('de', 'DE').currencies['COP'] == 'Kolumbianischer Peso' def test_currency_symbols_property(self): assert Locale('en', 'US').currency_symbols['USD'] == '$' assert Locale('es', 'CO').currency_symbols['USD'] == 'US$' def test_number_symbols_property(self): assert Locale('fr', 'FR').number_symbols["latn"]['decimal'] == ',' assert Locale('ar', 'IL').number_symbols["arab"]['percentSign'] == '٪\u061c' assert Locale('ar', 'IL').number_symbols["latn"]['percentSign'] == '\u200e%\u200e' def test_other_numbering_systems_property(self): assert Locale('fr', 'FR').other_numbering_systems['native'] == 'latn' assert 'traditional' not in Locale('fr', 'FR').other_numbering_systems assert Locale('el', 'GR').other_numbering_systems['native'] == 'latn' assert Locale('el', 'GR').other_numbering_systems['traditional'] == 'grek' def test_default_numbering_systems_property(self): assert Locale('en', 'GB').default_numbering_system == 'latn' assert Locale('ar', 'EG').default_numbering_system == 'arab' @pytest.mark.all_locales def test_all_locales_have_default_numbering_system(self, locale): locale = Locale.parse(locale) assert locale.default_numbering_system def test_decimal_formats(self): assert Locale('en', 'US').decimal_formats[None].pattern == '#,##0.###' def test_currency_formats_property(self): assert (Locale('en', 'US').currency_formats['standard'].pattern == '\xa4#,##0.00') assert (Locale('en', 'US').currency_formats['accounting'].pattern == '\xa4#,##0.00;(\xa4#,##0.00)') def test_percent_formats_property(self): assert Locale('en', 'US').percent_formats[None].pattern == '#,##0%' def test_scientific_formats_property(self): assert Locale('en', 'US').scientific_formats[None].pattern == '#E0' def test_periods_property(self): assert Locale('en', 'US').periods['am'] == 'AM' def test_days_property(self): assert Locale('de', 'DE').days['format']['wide'][3] == 'Donnerstag' def test_months_property(self): assert Locale('de', 'DE').months['format']['wide'][10] == 'Oktober' def test_quarters_property(self): assert Locale('de', 'DE').quarters['format']['wide'][1] == '1. Quartal' def test_eras_property(self): assert Locale('en', 'US').eras['wide'][1] == 'Anno Domini' assert Locale('en', 'US').eras['abbreviated'][0] == 'BC' def test_time_zones_property(self): time_zones = Locale('en', 'US').time_zones assert (time_zones['Europe/London']['long']['daylight'] == 'British Summer Time') assert time_zones['America/St_Johns']['city'] == 'St. John\u2019s' def test_meta_zones_property(self): meta_zones = Locale('en', 'US').meta_zones assert (meta_zones['Europe_Central']['long']['daylight'] == 'Central European Summer Time') def test_zone_formats_property(self): assert Locale('en', 'US').zone_formats['fallback'] == '%(1)s (%(0)s)' assert Locale('pt', 'BR').zone_formats['region'] == 'Hor\xe1rio %s' def test_first_week_day_property(self): assert Locale('de', 'DE').first_week_day == 0 assert Locale('en', 'US').first_week_day == 6 def test_weekend_start_property(self): assert Locale('de', 'DE').weekend_start == 5 def test_weekend_end_property(self): assert Locale('de', 'DE').weekend_end == 6 def test_min_week_days_property(self): assert Locale('de', 'DE').min_week_days == 4 def test_date_formats_property(self): assert Locale('en', 'US').date_formats['short'].pattern == 'M/d/yy' assert Locale('fr', 'FR').date_formats['long'].pattern == 'd MMMM y' def test_time_formats_property(self): assert Locale('en', 'US').time_formats['short'].pattern == 'h:mm\u202fa' assert Locale('fr', 'FR').time_formats['long'].pattern == 'HH:mm:ss z' def test_datetime_formats_property(self): assert Locale('en').datetime_formats['full'] == "{1}, {0}" assert Locale('th').datetime_formats['medium'] == '{1} {0}' def test_datetime_skeleton_property(self): assert Locale('en').datetime_skeletons['Md'].pattern == "M/d" assert Locale('th').datetime_skeletons['Md'].pattern == 'd/M' def test_plural_form_property(self): assert Locale('en').plural_form(1) == 'one' assert Locale('en').plural_form(0) == 'other' assert Locale('fr').plural_form(0) == 'one' assert Locale('ru').plural_form(100) == 'many' def test_default_locale(os_environ): for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']: os_environ[name] = '' os_environ['LANG'] = 'fr_FR.UTF-8' assert default_locale('LC_MESSAGES') == 'fr_FR' os_environ['LC_MESSAGES'] = 'POSIX' assert default_locale('LC_MESSAGES') == 'en_US_POSIX' for value in ['C', 'C.UTF-8', 'POSIX']: os_environ['LANGUAGE'] = value assert default_locale() == 'en_US_POSIX' def test_negotiate_locale(): assert (core.negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) == 'de_DE') assert core.negotiate_locale(['de_DE', 'en_US'], ['en', 'de']) == 'de' assert (core.negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) == 'de_DE') assert (core.negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) == 'de_DE') assert (core.negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US']) == 'ja_JP') assert core.negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE']) == 'nb_NO' def test_parse_locale(): assert core.parse_locale('zh_CN') == ('zh', 'CN', None, None) assert core.parse_locale('zh_Hans_CN') == ('zh', 'CN', 'Hans', None) assert core.parse_locale('zh-CN', sep='-') == ('zh', 'CN', None, None) with pytest.raises(ValueError) as excinfo: core.parse_locale('not_a_LOCALE_String') assert (excinfo.value.args[0] == "'not_a_LOCALE_String' is not a valid locale identifier") assert core.parse_locale('it_IT@euro') == ('it', 'IT', None, None, 'euro') assert core.parse_locale('it_IT@something') == ('it', 'IT', None, None, 'something') assert core.parse_locale('en_US.UTF-8') == ('en', 'US', None, None) assert (core.parse_locale('de_DE.iso885915@euro') == ('de', 'DE', None, None, 'euro')) @pytest.mark.parametrize('filename', [ 'babel/global.dat', 'babel/locale-data/root.dat', 'babel/locale-data/en.dat', 'babel/locale-data/en_US.dat', 'babel/locale-data/en_US_POSIX.dat', 'babel/locale-data/zh_Hans_CN.dat', 'babel/locale-data/zh_Hant_TW.dat', 'babel/locale-data/es_419.dat', ]) def test_compatible_classes_in_global_and_localedata(filename): import pickle class Unpickler(pickle.Unpickler): def find_class(self, module, name): # *.dat files must have compatible classes between Python 2 and 3 if module.split('.')[0] == 'babel': return pickle.Unpickler.find_class(self, module, name) raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") with open(filename, 'rb') as f: assert Unpickler(f).load() def test_issue_601_no_language_name_but_has_variant(): # kw_GB has a variant for Finnish but no actual language name for Finnish, # so `get_display_name()` previously crashed with a TypeError as it attempted # to concatenate " (Finnish)" to None. # Instead, it's better to return None altogether, as we can't reliably format # part of a language name. assert Locale.parse('fi_FI').get_display_name('kw_GB') is None def test_issue_814(): loc = Locale.parse('ca_ES_valencia') assert loc.variant == "VALENCIA" assert loc.get_display_name() == 'català (Espanya, valencià)' babel-2.14.0/tests/test_languages.py0000644000175000017500000000113714536056757016736 0ustar nileshnileshfrom babel.languages import get_official_languages, get_territory_language_info def test_official_languages(): assert get_official_languages("FI") == ("fi", "sv") assert get_official_languages("SE") == ("sv",) assert get_official_languages("CH") == ("de", "fr", "it") assert get_official_languages("CH", de_facto=True) == ("de", "gsw", "fr", "it") assert get_official_languages("CH", regional=True) == ("de", "fr", "it", "rm") def test_get_language_info(): assert ( set(get_territory_language_info("HU")) == {"hu", "fr", "en", "de", "ro", "hr", "sk", "sl"} ) babel-2.14.0/cldr/0000755000175000017500000000000014536056757013137 5ustar nileshnileshbabel-2.14.0/cldr/.gitignore0000644000175000017500000000000214536056757015117 0ustar nileshnilesh* babel-2.14.0/scripts/0000755000175000017500000000000014536056757013702 5ustar nileshnileshbabel-2.14.0/scripts/download_import_cldr.py0000755000175000017500000000520414536056757020465 0ustar nileshnilesh#!/usr/bin/env python3 import contextlib import hashlib import os import shutil import subprocess import sys import zipfile from urllib.request import urlretrieve URL = 'http://unicode.org/Public/cldr/43/cldr-common-43.0.zip' FILENAME = 'cldr-common-43.0.zip' # Via https://unicode.org/Public/cldr/43/hashes/SHASUM512 FILESUM = '930c64208d6f680d115bfa74a69445fb614910bb54233227b0b9ae85ddbce4db19e4ec863bf04ae9d4a11b2306aa7394e553384d7537487de8011f0e34877cef' BLKSIZE = 131072 def reporthook(block_count, block_size, total_size): bytes_transmitted = block_count * block_size cols = shutil.get_terminal_size().columns buffer = 6 percent = float(bytes_transmitted) / (total_size or 1) done = int(percent * (cols - buffer)) bar = ('=' * done).ljust(cols - buffer) sys.stdout.write(f'\r{bar}{int(percent * 100): 4d}%') sys.stdout.flush() def log(message): sys.stderr.write(f'{message}\n') def is_good_file(filename): if not os.path.isfile(filename): log(f"Local copy '{filename}' not found") return False h = hashlib.sha512() with open(filename, 'rb') as f: while True: blk = f.read(BLKSIZE) if not blk: break h.update(blk) digest = h.hexdigest() if digest != FILESUM: raise RuntimeError(f'Checksum mismatch: {digest!r} != {FILESUM!r}') else: return True def main(): scripts_path = os.path.dirname(os.path.abspath(__file__)) repo = os.path.dirname(scripts_path) cldr_dl_path = os.path.join(repo, 'cldr') cldr_path = os.path.join(repo, 'cldr', os.path.splitext(FILENAME)[0]) zip_path = os.path.join(cldr_dl_path, FILENAME) changed = False show_progress = (False if os.environ.get("BABEL_CLDR_NO_DOWNLOAD_PROGRESS") else sys.stdout.isatty()) while not is_good_file(zip_path): log(f"Downloading '{FILENAME}' from {URL}") tmp_path = f"{zip_path}.tmp" urlretrieve(URL, tmp_path, (reporthook if show_progress else None)) os.replace(tmp_path, zip_path) changed = True print() common_path = os.path.join(cldr_path, 'common') if changed or not os.path.isdir(common_path): if os.path.isdir(common_path): log(f"Deleting old CLDR checkout in '{cldr_path}'") shutil.rmtree(common_path) log(f"Extracting CLDR to '{cldr_path}'") with contextlib.closing(zipfile.ZipFile(zip_path)) as z: z.extractall(cldr_path) subprocess.check_call([ sys.executable, os.path.join(scripts_path, 'import_cldr.py'), common_path]) if __name__ == '__main__': main() babel-2.14.0/scripts/dump_data.py0000755000175000017500000000273614536056757016225 0ustar nileshnilesh#!/usr/bin/env python # # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. from optparse import OptionParser from pprint import pprint from babel.localedata import LocaleDataDict, load def main(): parser = OptionParser(usage='%prog [options] locale [path]') parser.add_option('--noinherit', action='store_false', dest='inherit', help='do not merge inherited data into locale data') parser.add_option('--resolve', action='store_true', dest='resolve', help='resolve aliases in locale data') parser.set_defaults(inherit=True, resolve=False) options, args = parser.parse_args() if len(args) not in (1, 2): parser.error('incorrect number of arguments') data = load(args[0], merge_inherited=options.inherit) if options.resolve: data = LocaleDataDict(data) if len(args) > 1: for key in args[1].split('.'): data = data[key] if isinstance(data, dict): data = dict(data.items()) pprint(data) if __name__ == '__main__': main() babel-2.14.0/scripts/dump_global.py0000755000175000017500000000153414536056757016547 0ustar nileshnilesh#!/usr/bin/env python # # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import os import sys from pprint import pprint import cPickle as pickle import babel dirname = os.path.join(os.path.dirname(babel.__file__)) filename = os.path.join(dirname, 'global.dat') with open(filename, 'rb') as fileobj: data = pickle.load(fileobj) if len(sys.argv) > 1: pprint(data.get(sys.argv[1])) else: pprint(data) babel-2.14.0/scripts/generate_authors.py0000644000175000017500000000234214536056757017614 0ustar nileshnileshimport os from collections import Counter from subprocess import check_output root_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) def get_sorted_authors_list(): authors = check_output(['git', 'log', '--format=%aN'], cwd=root_path).decode('UTF-8') counts = Counter(authors.splitlines()) return [author for (author, count) in counts.most_common()] def get_authors_file_content(): author_list = "\n".join(f"- {a}" for a in get_sorted_authors_list()) return f''' Babel is written and maintained by the Babel team and various contributors: {author_list} Babel was previously developed under the Copyright of Edgewall Software. The following copyright notice holds true for releases before 2013: "Copyright (c) 2007 - 2011 by Edgewall Software" In addition to the regular contributions Babel includes a fork of Lennart Regebro's tzlocal that originally was licensed under the CC0 license. The original copyright of that project is "Copyright 2013 by Lennart Regebro". ''' def write_authors_file(): content = get_authors_file_content() with open(os.path.join(root_path, 'AUTHORS'), 'w', encoding='UTF-8') as fp: fp.write(content) if __name__ == '__main__': write_authors_file() babel-2.14.0/scripts/import_cldr.py0000755000175000017500000012420314536056757016577 0ustar nileshnilesh#!/usr/bin/env python # # Copyright (C) 2007-2011 Edgewall Software, 2013-2023 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. import collections import logging import os import pickle import re import sys from optparse import OptionParser from xml.etree import ElementTree # Make sure we're using Babel source, and not some previously installed version CHECKOUT_ROOT = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', )) BABEL_PACKAGE_ROOT = os.path.join(CHECKOUT_ROOT, "babel") sys.path.insert(0, CHECKOUT_ROOT) from babel import dates, numbers from babel.dates import split_interval_pattern from babel.localedata import Alias from babel.plural import PluralRule parse = ElementTree.parse weekdays = {'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5, 'sun': 6} def _text(elem): buf = [elem.text or ''] for child in elem: buf.append(_text(child)) buf.append(elem.tail or '') return ''.join(filter(None, buf)).strip() NAME_RE = re.compile(r"^\w+$") TYPE_ATTR_RE = re.compile(r"^\w+\[@type='(.*?)'\]$") NAME_MAP = { 'dateFormats': 'date_formats', 'dateTimeFormats': 'datetime_formats', 'eraAbbr': 'abbreviated', 'eraNames': 'wide', 'eraNarrow': 'narrow', 'timeFormats': 'time_formats', } log = logging.getLogger("import_cldr") def need_conversion(dst_filename, data_dict, source_filename): with open(source_filename, 'rb') as f: blob = f.read(4096) version_match = re.search(b'version number="\\$Revision: (\\d+)', blob) if not version_match: # CLDR 36.0 was shipped without proper revision numbers return True version = int(version_match.group(1)) data_dict['_version'] = version if not os.path.isfile(dst_filename): return True with open(dst_filename, 'rb') as f: data = pickle.load(f) return data.get('_version') != version def _translate_alias(ctxt, path): parts = path.split('/') keys = ctxt[:] for part in parts: if part == '..': keys.pop() else: match = TYPE_ATTR_RE.match(part) if match: keys.append(match.group(1)) else: assert NAME_RE.match(part) keys.append(NAME_MAP.get(part, part)) return keys def _parse_currency_date(s): if not s: return None parts = s.split('-', 2) return tuple(map(int, parts + [1] * (3 - len(parts)))) def _currency_sort_key(tup): code, start, end, tender = tup return int(not tender), start or (1, 1, 1) def _extract_plural_rules(file_path): rule_dict = {} prsup = parse(file_path) for elem in prsup.findall('.//plurals/pluralRules'): rules = [] for rule in elem.findall('pluralRule'): rules.append((rule.attrib['count'], str(rule.text))) pr = PluralRule(rules) for locale in elem.attrib['locales'].split(): rule_dict[locale] = pr return rule_dict def _time_to_seconds_past_midnight(time_expr): """ Parse a time expression to seconds after midnight. :param time_expr: Time expression string (H:M or H:M:S) :rtype: int """ if time_expr is None: return None if time_expr.count(":") == 1: time_expr += ":00" hour, minute, second = (int(p, 10) for p in time_expr.split(":")) return hour * 60 * 60 + minute * 60 + second def _compact_dict(dict): """ "Compact" the given dict by removing items whose value is None or False. """ out_dict = {} for key, value in dict.items(): if value is not None and value is not False: out_dict[key] = value return out_dict def debug_repr(obj): if isinstance(obj, PluralRule): return obj.abstract return repr(obj) def write_datafile(path, data, dump_json=False): with open(path, 'wb') as outfile: pickle.dump(data, outfile, 2) if dump_json: import json with open(f"{path}.json", "w") as outfile: json.dump(data, outfile, indent=4, default=debug_repr) def main(): parser = OptionParser(usage='%prog path/to/cldr') parser.add_option( '-f', '--force', dest='force', action='store_true', default=False, help='force import even if destination file seems up to date', ) parser.add_option( '-j', '--json', dest='dump_json', action='store_true', default=False, help='also export debugging JSON dumps of locale data', ) parser.add_option( '-q', '--quiet', dest='quiet', action='store_true', default=bool(os.environ.get('BABEL_CLDR_QUIET')), help='quiesce info/warning messages', ) options, args = parser.parse_args() if len(args) != 1: parser.error('incorrect number of arguments') logging.basicConfig( level=(logging.ERROR if options.quiet else logging.INFO), ) return process_data( srcdir=args[0], destdir=BABEL_PACKAGE_ROOT, force=bool(options.force), dump_json=bool(options.dump_json), ) def process_data(srcdir, destdir, force=False, dump_json=False): sup_filename = os.path.join(srcdir, 'supplemental', 'supplementalData.xml') sup = parse(sup_filename) # Import global data from the supplemental files global_path = os.path.join(destdir, 'global.dat') global_data = {} if force or need_conversion(global_path, global_data, sup_filename): global_data.update(parse_global(srcdir, sup)) write_datafile(global_path, global_data, dump_json=dump_json) _process_local_datas(sup, srcdir, destdir, force=force, dump_json=dump_json) def parse_global(srcdir, sup): global_data = {} sup_dir = os.path.join(srcdir, 'supplemental') territory_zones = global_data.setdefault('territory_zones', {}) zone_aliases = global_data.setdefault('zone_aliases', {}) zone_territories = global_data.setdefault('zone_territories', {}) win_mapping = global_data.setdefault('windows_zone_mapping', {}) language_aliases = global_data.setdefault('language_aliases', {}) territory_aliases = global_data.setdefault('territory_aliases', {}) script_aliases = global_data.setdefault('script_aliases', {}) variant_aliases = global_data.setdefault('variant_aliases', {}) likely_subtags = global_data.setdefault('likely_subtags', {}) territory_currencies = global_data.setdefault('territory_currencies', {}) parent_exceptions = global_data.setdefault('parent_exceptions', {}) all_currencies = collections.defaultdict(set) currency_fractions = global_data.setdefault('currency_fractions', {}) territory_languages = global_data.setdefault('territory_languages', {}) bcp47_timezone = parse(os.path.join(srcdir, 'bcp47', 'timezone.xml')) sup_windows_zones = parse(os.path.join(sup_dir, 'windowsZones.xml')) sup_metadata = parse(os.path.join(sup_dir, 'supplementalMetadata.xml')) sup_likely = parse(os.path.join(sup_dir, 'likelySubtags.xml')) # create auxiliary zone->territory map from the windows zones (we don't set # the 'zones_territories' map directly here, because there are some zones # aliases listed and we defer the decision of which ones to choose to the # 'bcp47' data _zone_territory_map = {} for map_zone in sup_windows_zones.findall('.//windowsZones/mapTimezones/mapZone'): if map_zone.attrib.get('territory') == '001': win_mapping[map_zone.attrib['other']] = map_zone.attrib['type'].split()[0] for tzid in str(map_zone.attrib['type']).split(): _zone_territory_map[tzid] = str(map_zone.attrib['territory']) for key_elem in bcp47_timezone.findall('.//keyword/key'): if key_elem.attrib['name'] == 'tz': for elem in key_elem.findall('type'): if 'deprecated' not in elem.attrib: aliases = str(elem.attrib['alias']).split() tzid = aliases.pop(0) territory = _zone_territory_map.get(tzid, '001') territory_zones.setdefault(territory, []).append(tzid) zone_territories[tzid] = territory for alias in aliases: zone_aliases[alias] = tzid break # Import Metazone mapping meta_zones = global_data.setdefault('meta_zones', {}) tzsup = parse(os.path.join(srcdir, 'supplemental', 'metaZones.xml')) for elem in tzsup.findall('.//timezone'): for child in elem.findall('usesMetazone'): if 'to' not in child.attrib: # FIXME: support old mappings meta_zones[elem.attrib['type']] = child.attrib['mzone'] # Language aliases for alias in sup_metadata.findall('.//alias/languageAlias'): # We don't have a use for those at the moment. They don't # pass our parser anyways. if '_' in alias.attrib['type']: continue language_aliases[alias.attrib['type']] = alias.attrib['replacement'] # Territory aliases for alias in sup_metadata.findall('.//alias/territoryAlias'): territory_aliases[alias.attrib['type']] = alias.attrib['replacement'].split() # Script aliases for alias in sup_metadata.findall('.//alias/scriptAlias'): script_aliases[alias.attrib['type']] = alias.attrib['replacement'] # Variant aliases for alias in sup_metadata.findall('.//alias/variantAlias'): repl = alias.attrib.get('replacement') if repl: variant_aliases[alias.attrib['type']] = repl # Likely subtags for likely_subtag in sup_likely.findall('.//likelySubtags/likelySubtag'): likely_subtags[likely_subtag.attrib['from']] = likely_subtag.attrib['to'] # Currencies in territories for region in sup.findall('.//currencyData/region'): region_code = region.attrib['iso3166'] region_currencies = [] for currency in region.findall('./currency'): cur_code = currency.attrib['iso4217'] cur_start = _parse_currency_date(currency.attrib.get('from')) cur_end = _parse_currency_date(currency.attrib.get('to')) cur_tender = currency.attrib.get('tender', 'true') == 'true' # Tie region to currency. region_currencies.append((cur_code, cur_start, cur_end, cur_tender)) # Keep a reverse index of currencies to territorie. all_currencies[cur_code].add(region_code) region_currencies.sort(key=_currency_sort_key) territory_currencies[region_code] = region_currencies global_data['all_currencies'] = { currency: tuple(sorted(regions)) for currency, regions in all_currencies.items()} # Explicit parent locales # Since CLDR-43, there are multiple statements, some of them with a `component="collations"` or # `component="segmentations"` attribute; these indicate that only some language aspects should be inherited. # (https://cldr.unicode.org/index/downloads/cldr-43) # # Ignore these for now, as one of them even points to a locale that doesn't have a corresponding XML file (sr_ME) # and we crash trying to load it. # There is no XPath support to test for an absent attribute, so use Python to filter for parentBlock in sup.findall('.//parentLocales'): if parentBlock.attrib.get('component'): # Consider only unqualified parent declarations continue for paternity in parentBlock.findall('./parentLocale'): parent = paternity.attrib['parent'] for child in paternity.attrib['locales'].split(): parent_exceptions[child] = parent # Currency decimal and rounding digits for fraction in sup.findall('.//currencyData/fractions/info'): cur_code = fraction.attrib['iso4217'] cur_digits = int(fraction.attrib['digits']) cur_rounding = int(fraction.attrib['rounding']) cur_cdigits = int(fraction.attrib.get('cashDigits', cur_digits)) cur_crounding = int(fraction.attrib.get('cashRounding', cur_rounding)) currency_fractions[cur_code] = (cur_digits, cur_rounding, cur_cdigits, cur_crounding) # Languages in territories for territory in sup.findall('.//territoryInfo/territory'): languages = {} for language in territory.findall('./languagePopulation'): languages[language.attrib['type']] = { 'population_percent': float(language.attrib['populationPercent']), 'official_status': language.attrib.get('officialStatus'), } territory_languages[territory.attrib['type']] = languages return global_data def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): day_period_rules = parse_day_period_rules(parse(os.path.join(srcdir, 'supplemental', 'dayPeriods.xml'))) # build a territory containment mapping for inheritance regions = {} for elem in sup.findall('.//territoryContainment/group'): regions[elem.attrib['type']] = elem.attrib['contains'].split() # Resolve territory containment territory_containment = {} region_items = sorted(regions.items()) for group, territory_list in region_items: for territory in territory_list: containers = territory_containment.setdefault(territory, set()) if group in territory_containment: containers |= territory_containment[group] containers.add(group) # prepare the per-locale plural rules definitions plural_rules = _extract_plural_rules(os.path.join(srcdir, 'supplemental', 'plurals.xml')) ordinal_rules = _extract_plural_rules(os.path.join(srcdir, 'supplemental', 'ordinals.xml')) filenames = os.listdir(os.path.join(srcdir, 'main')) filenames.remove('root.xml') filenames.sort(key=len) filenames.insert(0, 'root.xml') for filename in filenames: stem, ext = os.path.splitext(filename) if ext != '.xml': continue full_filename = os.path.join(srcdir, "main", filename) data_filename = os.path.join(destdir, "locale-data", f"{stem}.dat") data = {} if not (force or need_conversion(data_filename, data, full_filename)): continue tree = parse(full_filename) language = None elem = tree.find('.//identity/language') if elem is not None: language = elem.attrib['type'] territory = None elem = tree.find('.//identity/territory') if elem is not None: territory = elem.attrib['type'] else: territory = '001' # world regions = territory_containment.get(territory, []) log.info( 'Processing %s (Language = %s; Territory = %s)', filename, language, territory, ) locale_id = '_'.join(filter(None, [ language, territory != '001' and territory or None, ])) data['locale_id'] = locale_id data['unsupported_number_systems'] = set() if locale_id in plural_rules: data['plural_form'] = plural_rules[locale_id] if locale_id in ordinal_rules: data['ordinal_form'] = ordinal_rules[locale_id] if locale_id in day_period_rules: data["day_period_rules"] = day_period_rules[locale_id] parse_locale_display_names(data, tree) parse_list_patterns(data, tree) parse_dates(data, tree, sup, regions, territory) for calendar in tree.findall('.//calendars/calendar'): if calendar.attrib['type'] != 'gregorian': # TODO: support other calendar types continue parse_calendar_months(data, calendar) parse_calendar_days(data, calendar) parse_calendar_quarters(data, calendar) parse_calendar_eras(data, calendar) parse_calendar_periods(data, calendar) parse_calendar_date_formats(data, calendar) parse_calendar_time_formats(data, calendar) parse_calendar_datetime_skeletons(data, calendar) parse_interval_formats(data, calendar) parse_number_symbols(data, tree) parse_numbering_systems(data, tree) parse_decimal_formats(data, tree) parse_scientific_formats(data, tree) parse_percent_formats(data, tree) parse_currency_formats(data, tree) parse_currency_unit_patterns(data, tree) parse_currency_names(data, tree) parse_unit_patterns(data, tree) parse_date_fields(data, tree) parse_character_order(data, tree) parse_measurement_systems(data, tree) unsupported_number_systems_string = ', '.join(sorted(data.pop('unsupported_number_systems'))) if unsupported_number_systems_string: log.warning( f"{locale_id}: unsupported number systems were ignored: " f"{unsupported_number_systems_string}", ) write_datafile(data_filename, data, dump_json=dump_json) def _should_skip_number_elem(data, elem): """ Figure out whether the numbering-containing element `elem` is in a currently non-supported (i.e. currently non-Latin) numbering system. :param data: The root data element, for stashing the warning. :param elem: Element with `numberSystem` key :return: Boolean """ number_system = elem.get('numberSystem', 'latn') if number_system != 'latn': data['unsupported_number_systems'].add(number_system) return True return False def _should_skip_elem(elem, type=None, dest=None): """ Check whether the given element should be skipped. Elements are skipped if they are drafts or alternates of data that already exists in `dest`. :param elem: XML element :param type: Type string. May be elided if the dest dict is elided. :param dest: Destination dict. May be elided to skip the dict check. :return: skip boolean """ if 'draft' in elem.attrib or 'alt' in elem.attrib: if dest is None or type in dest: return True def _import_type_text(dest, elem, type=None): """ Conditionally import the element's inner text(s) into the `dest` dict. The condition being, namely, that the element isn't a draft/alternate version of a pre-existing element. :param dest: Destination dict :param elem: XML element. :param type: Override type. (By default, the `type` attr of the element.) :return: """ if type is None: type = elem.attrib['type'] if _should_skip_elem(elem, type, dest): return dest[type] = _text(elem) def parse_locale_display_names(data, tree): territories = data.setdefault('territories', {}) for elem in tree.findall('.//territories/territory'): _import_type_text(territories, elem) languages = data.setdefault('languages', {}) for elem in tree.findall('.//languages/language'): _import_type_text(languages, elem) variants = data.setdefault('variants', {}) for elem in tree.findall('.//variants/variant'): _import_type_text(variants, elem) scripts = data.setdefault('scripts', {}) for elem in tree.findall('.//scripts/script'): _import_type_text(scripts, elem) def parse_list_patterns(data, tree): list_patterns = data.setdefault('list_patterns', {}) for listType in tree.findall('.//listPatterns/listPattern'): by_type = list_patterns.setdefault(listType.attrib.get('type', 'standard'), {}) for listPattern in listType.findall('listPatternPart'): by_type[listPattern.attrib['type']] = _text(listPattern) def parse_dates(data, tree, sup, regions, territory): week_data = data.setdefault('week_data', {}) supelem = sup.find('.//weekData') for elem in supelem.findall('minDays'): if _should_skip_elem(elem): continue territories = elem.attrib['territories'].split() if territory in territories or any(r in territories for r in regions): week_data['min_days'] = int(elem.attrib['count']) for elem in supelem.findall('firstDay'): if _should_skip_elem(elem): continue territories = elem.attrib['territories'].split() if territory in territories or any(r in territories for r in regions): week_data['first_day'] = weekdays[elem.attrib['day']] for elem in supelem.findall('weekendStart'): if _should_skip_elem(elem): continue territories = elem.attrib['territories'].split() if territory in territories or any(r in territories for r in regions): week_data['weekend_start'] = weekdays[elem.attrib['day']] for elem in supelem.findall('weekendEnd'): if _should_skip_elem(elem): continue territories = elem.attrib['territories'].split() if territory in territories or any(r in territories for r in regions): week_data['weekend_end'] = weekdays[elem.attrib['day']] zone_formats = data.setdefault('zone_formats', {}) for elem in tree.findall('.//timeZoneNames/gmtFormat'): if not _should_skip_elem(elem): zone_formats['gmt'] = str(elem.text).replace('{0}', '%s') break for elem in tree.findall('.//timeZoneNames/regionFormat'): if not _should_skip_elem(elem): zone_formats['region'] = str(elem.text).replace('{0}', '%s') break for elem in tree.findall('.//timeZoneNames/fallbackFormat'): if not _should_skip_elem(elem): zone_formats['fallback'] = ( str(elem.text).replace('{0}', '%(0)s').replace('{1}', '%(1)s') ) break for elem in tree.findall('.//timeZoneNames/fallbackRegionFormat'): if not _should_skip_elem(elem): zone_formats['fallback_region'] = ( str(elem.text).replace('{0}', '%(0)s').replace('{1}', '%(1)s') ) break time_zones = data.setdefault('time_zones', {}) for elem in tree.findall('.//timeZoneNames/zone'): info = {} city = elem.findtext('exemplarCity') if city: info['city'] = str(city) for child in elem.findall('long/*'): info.setdefault('long', {})[child.tag] = str(child.text) for child in elem.findall('short/*'): info.setdefault('short', {})[child.tag] = str(child.text) time_zones[elem.attrib['type']] = info meta_zones = data.setdefault('meta_zones', {}) for elem in tree.findall('.//timeZoneNames/metazone'): info = {} city = elem.findtext('exemplarCity') if city: info['city'] = str(city) for child in elem.findall('long/*'): info.setdefault('long', {})[child.tag] = str(child.text) for child in elem.findall('short/*'): info.setdefault('short', {})[child.tag] = str(child.text) meta_zones[elem.attrib['type']] = info def parse_calendar_months(data, calendar): months = data.setdefault('months', {}) for ctxt in calendar.findall('months/monthContext'): ctxt_type = ctxt.attrib['type'] ctxts = months.setdefault(ctxt_type, {}) for width in ctxt.findall('monthWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) for elem in width: if elem.tag == 'month': _import_type_text(widths, elem, int(elem.attrib['type'])) elif elem.tag == 'alias': ctxts[width_type] = Alias( _translate_alias(['months', ctxt_type, width_type], elem.attrib['path']), ) def parse_calendar_days(data, calendar): days = data.setdefault('days', {}) for ctxt in calendar.findall('days/dayContext'): ctxt_type = ctxt.attrib['type'] ctxts = days.setdefault(ctxt_type, {}) for width in ctxt.findall('dayWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) for elem in width: if elem.tag == 'day': _import_type_text(widths, elem, weekdays[elem.attrib['type']]) elif elem.tag == 'alias': ctxts[width_type] = Alias( _translate_alias(['days', ctxt_type, width_type], elem.attrib['path']), ) def parse_calendar_quarters(data, calendar): quarters = data.setdefault('quarters', {}) for ctxt in calendar.findall('quarters/quarterContext'): ctxt_type = ctxt.attrib['type'] ctxts = quarters.setdefault(ctxt.attrib['type'], {}) for width in ctxt.findall('quarterWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) for elem in width: if elem.tag == 'quarter': _import_type_text(widths, elem, int(elem.attrib['type'])) elif elem.tag == 'alias': ctxts[width_type] = Alias( _translate_alias(['quarters', ctxt_type, width_type], elem.attrib['path'])) def parse_calendar_eras(data, calendar): eras = data.setdefault('eras', {}) for width in calendar.findall('eras/*'): width_type = NAME_MAP[width.tag] widths = eras.setdefault(width_type, {}) for elem in width: if elem.tag == 'era': _import_type_text(widths, elem, type=int(elem.attrib.get('type'))) elif elem.tag == 'alias': eras[width_type] = Alias( _translate_alias(['eras', width_type], elem.attrib['path']), ) def parse_calendar_periods(data, calendar): # Day periods (AM/PM/others) periods = data.setdefault('day_periods', {}) for day_period_ctx in calendar.findall('dayPeriods/dayPeriodContext'): ctx_type = day_period_ctx.attrib["type"] for day_period_width in day_period_ctx.findall('dayPeriodWidth'): width_type = day_period_width.attrib["type"] dest_dict = periods.setdefault(ctx_type, {}).setdefault(width_type, {}) for day_period in day_period_width.findall('dayPeriod'): period_type = day_period.attrib['type'] if 'alt' not in day_period.attrib: dest_dict[period_type] = str(day_period.text) def parse_calendar_date_formats(data, calendar): date_formats = data.setdefault('date_formats', {}) for format in calendar.findall('dateFormats'): for elem in format: if elem.tag == 'dateFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, date_formats): continue try: date_formats[type] = dates.parse_pattern( str(elem.findtext('dateFormat/pattern')), ) except ValueError as e: log.error(e) elif elem.tag == 'alias': date_formats = Alias(_translate_alias( ['date_formats'], elem.attrib['path']), ) def parse_calendar_time_formats(data, calendar): time_formats = data.setdefault('time_formats', {}) for format in calendar.findall('timeFormats'): for elem in format: if elem.tag == 'timeFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, time_formats): continue try: time_formats[type] = dates.parse_pattern( str(elem.findtext('timeFormat/pattern')), ) except ValueError as e: log.error(e) elif elem.tag == 'alias': time_formats = Alias(_translate_alias( ['time_formats'], elem.attrib['path']), ) def parse_calendar_datetime_skeletons(data, calendar): datetime_formats = data.setdefault('datetime_formats', {}) datetime_skeletons = data.setdefault('datetime_skeletons', {}) for format in calendar.findall('dateTimeFormats'): for elem in format: if elem.tag == 'dateTimeFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, datetime_formats): continue try: datetime_formats[type] = str(elem.findtext('dateTimeFormat/pattern')) except ValueError as e: log.error(e) elif elem.tag == 'alias': datetime_formats = Alias(_translate_alias( ['datetime_formats'], elem.attrib['path']), ) elif elem.tag == 'availableFormats': for datetime_skeleton in elem.findall('dateFormatItem'): datetime_skeletons[datetime_skeleton.attrib['id']] = ( dates.parse_pattern(str(datetime_skeleton.text)) ) def parse_number_symbols(data, tree): number_symbols = data.setdefault('number_symbols', {}) for symbol_system_elem in tree.findall('.//numbers/symbols'): number_system = symbol_system_elem.get('numberSystem') if not number_system: continue for symbol_element in symbol_system_elem.findall('./*'): if _should_skip_elem(symbol_element): continue number_symbols.setdefault(number_system, {})[symbol_element.tag] = str(symbol_element.text) def parse_numbering_systems(data, tree): default_number_system_node = tree.find('.//numbers/defaultNumberingSystem') if default_number_system_node is not None: data['default_numbering_system'] = default_number_system_node.text numbering_systems = data.setdefault('numbering_systems', {}) other_numbering_systems_node = tree.find('.//numbers/otherNumberingSystems') or [] for system in other_numbering_systems_node: numbering_systems[system.tag] = system.text def parse_decimal_formats(data, tree): decimal_formats = data.setdefault('decimal_formats', {}) for df_elem in tree.findall('.//decimalFormats'): if _should_skip_number_elem(data, df_elem): # TODO: Support other number systems continue for elem in df_elem.findall('./decimalFormatLength'): length_type = elem.attrib.get('type') if _should_skip_elem(elem, length_type, decimal_formats): continue if elem.findall('./alias'): # TODO map the alias to its target continue for pattern_el in elem.findall('./decimalFormat/pattern'): pattern_type = pattern_el.attrib.get('type') pattern = numbers.parse_pattern(str(pattern_el.text)) if pattern_type: # This is a compact decimal format, see: # https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats # These are mapped into a `compact_decimal_formats` dictionary # with the format {length: {count: {multiplier: pattern}}}. compact_decimal_formats = data.setdefault('compact_decimal_formats', {}) length_map = compact_decimal_formats.setdefault(length_type, {}) length_count_map = length_map.setdefault(pattern_el.attrib['count'], {}) length_count_map[pattern_type] = pattern else: # Regular decimal format. decimal_formats[length_type] = pattern def parse_scientific_formats(data, tree): scientific_formats = data.setdefault('scientific_formats', {}) for sf_elem in tree.findall('.//scientificFormats'): if _should_skip_number_elem(data, sf_elem): # TODO: Support other number systems continue for elem in sf_elem.findall('./scientificFormatLength'): type = elem.attrib.get('type') if _should_skip_elem(elem, type, scientific_formats): continue pattern = str(elem.findtext('scientificFormat/pattern')) scientific_formats[type] = numbers.parse_pattern(pattern) def parse_percent_formats(data, tree): percent_formats = data.setdefault('percent_formats', {}) for pf_elem in tree.findall('.//percentFormats'): if _should_skip_number_elem(data, pf_elem): # TODO: Support other number systems continue for elem in pf_elem.findall('.//percentFormatLength'): type = elem.attrib.get('type') if _should_skip_elem(elem, type, percent_formats): continue pattern = str(elem.findtext('percentFormat/pattern')) percent_formats[type] = numbers.parse_pattern(pattern) def parse_currency_names(data, tree): currency_names = data.setdefault('currency_names', {}) currency_names_plural = data.setdefault('currency_names_plural', {}) currency_symbols = data.setdefault('currency_symbols', {}) for elem in tree.findall('.//currencies/currency'): code = elem.attrib['type'] for name in elem.findall('displayName'): if ('draft' in name.attrib) and code in currency_names: continue if 'count' in name.attrib: currency_names_plural.setdefault(code, {})[ name.attrib['count']] = str(name.text) else: currency_names[code] = str(name.text) for symbol in elem.findall('symbol'): if 'draft' in symbol.attrib or 'choice' in symbol.attrib: # Skip drafts and choice-patterns continue if symbol.attrib.get('alt'): # Skip alternate forms continue currency_symbols[code] = str(symbol.text) def parse_unit_patterns(data, tree): unit_patterns = data.setdefault('unit_patterns', {}) compound_patterns = data.setdefault('compound_unit_patterns', {}) unit_display_names = data.setdefault('unit_display_names', {}) for elem in tree.findall('.//units/unitLength'): unit_length_type = elem.attrib['type'] for unit in elem.findall('unit'): unit_type = unit.attrib['type'] unit_and_length_patterns = unit_patterns.setdefault(unit_type, {}).setdefault(unit_length_type, {}) for pattern in unit.findall('unitPattern'): if pattern.attrib.get('case', 'nominative') != 'nominative': # Skip non-nominative cases. continue unit_and_length_patterns[pattern.attrib['count']] = _text(pattern) per_unit_pat = unit.find('perUnitPattern') if per_unit_pat is not None: unit_and_length_patterns['per'] = _text(per_unit_pat) display_name = unit.find('displayName') if display_name is not None: unit_display_names.setdefault(unit_type, {})[unit_length_type] = _text(display_name) for unit in elem.findall('compoundUnit'): unit_type = unit.attrib['type'] compound_unit_info = {} compound_variations = {} for child in unit: if child.attrib.get('case', 'nominative') != 'nominative': # Skip non-nominative cases. continue if child.tag == "unitPrefixPattern": compound_unit_info['prefix'] = _text(child) elif child.tag == "compoundUnitPattern": compound_variations[None] = _text(child) elif child.tag == "compoundUnitPattern1": compound_variations[child.attrib.get('count')] = _text(child) if compound_variations: compound_variation_values = set(compound_variations.values()) if len(compound_variation_values) == 1: # shortcut: if all compound variations are the same, only store one compound_unit_info['compound'] = next(iter(compound_variation_values)) else: compound_unit_info['compound_variations'] = compound_variations compound_patterns.setdefault(unit_type, {})[unit_length_type] = compound_unit_info def parse_date_fields(data, tree): date_fields = data.setdefault('date_fields', {}) for elem in tree.findall('.//dates/fields/field'): field_type = elem.attrib['type'] date_fields.setdefault(field_type, {}) for rel_time in elem.findall('relativeTime'): rel_time_type = rel_time.attrib['type'] for pattern in rel_time.findall('relativeTimePattern'): type_dict = date_fields[field_type].setdefault(rel_time_type, {}) type_dict[pattern.attrib['count']] = str(pattern.text) def parse_interval_formats(data, tree): # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats interval_formats = data.setdefault("interval_formats", {}) for elem in tree.findall("dateTimeFormats/intervalFormats/*"): if 'draft' in elem.attrib: continue if elem.tag == "intervalFormatFallback": interval_formats[None] = elem.text elif elem.tag == "intervalFormatItem": skel_data = interval_formats.setdefault(elem.attrib["id"], {}) for item_sub in elem: if item_sub.tag == "greatestDifference": skel_data[item_sub.attrib["id"]] = split_interval_pattern(item_sub.text) else: raise NotImplementedError(f"Not implemented: {item_sub.tag}({item_sub.attrib!r})") def parse_currency_formats(data, tree): currency_formats = data.setdefault('currency_formats', {}) for currency_format in tree.findall('.//currencyFormats'): if _should_skip_number_elem(data, currency_format): # TODO: Support other number systems continue for length_elem in currency_format.findall('./currencyFormatLength'): curr_length_type = length_elem.attrib.get('type') for elem in length_elem.findall('currencyFormat'): type = elem.attrib.get('type') if _should_skip_elem(elem, type, currency_formats): continue for child in elem.iter(): if child.tag == 'alias': currency_formats[type] = Alias( _translate_alias(['currency_formats', elem.attrib['type']], child.attrib['path']), ) elif child.tag == 'pattern': pattern_type = child.attrib.get('type') if child.attrib.get('draft') or child.attrib.get('alt'): # Skip drafts and alternates. # The `noCurrency` alternate for currencies was added in CLDR 42. continue pattern = numbers.parse_pattern(str(child.text)) if pattern_type: # This is a compact currency format, see: # https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats # These are mapped into a `compact_currency_formats` dictionary # with the format {length: {count: {multiplier: pattern}}}. compact_currency_formats = data.setdefault('compact_currency_formats', {}) length_map = compact_currency_formats.setdefault(curr_length_type, {}) length_count_map = length_map.setdefault(child.attrib['count'], {}) length_count_map[pattern_type] = pattern else: # Regular currency format currency_formats[type] = pattern def parse_currency_unit_patterns(data, tree): currency_unit_patterns = data.setdefault('currency_unit_patterns', {}) for currency_formats_elem in tree.findall('.//currencyFormats'): if _should_skip_number_elem(data, currency_formats_elem): # TODO: Support other number systems continue for unit_pattern_elem in currency_formats_elem.findall('./unitPattern'): count = unit_pattern_elem.attrib['count'] pattern = str(unit_pattern_elem.text) currency_unit_patterns[count] = pattern def parse_day_period_rules(tree): """ Parse dayPeriodRule data into a dict. :param tree: ElementTree """ day_periods = {} for ruleset in tree.findall(".//dayPeriodRuleSet"): ruleset_type = ruleset.attrib.get("type") # None|"selection" for rules in ruleset.findall("dayPeriodRules"): locales = rules.attrib["locales"].split() for rule in rules.findall("dayPeriodRule"): type = rule.attrib["type"] if type in ("am", "pm"): # These fixed periods are handled separately by `get_period_id` continue rule = _compact_dict({ key: _time_to_seconds_past_midnight(rule.attrib.get(key)) for key in ("after", "at", "before", "from", "to") }) for locale in locales: dest_list = day_periods.setdefault(locale, {}).setdefault(ruleset_type, {}).setdefault(type, []) dest_list.append(rule) return day_periods def parse_character_order(data, tree): for elem in tree.findall('.//layout/orientation/characterOrder'): data['character_order'] = elem.text def parse_measurement_systems(data, tree): measurement_systems = data.setdefault('measurement_systems', {}) for measurement_system in tree.findall('.//measurementSystemNames/measurementSystemName'): type = measurement_system.attrib['type'] if not _should_skip_elem(measurement_system, type=type, dest=measurement_systems): _import_type_text(measurement_systems, measurement_system, type=type) if __name__ == '__main__': main() babel-2.14.0/pyproject.toml0000644000175000017500000000071314536056757015130 0ustar nileshnilesh[tool.ruff] target-version = "py37" select = [ "B", "C", "COM", "E", "F", "I", "SIM300", "UP", ] ignore = [ "C901", # Complexity "E501", # Line length "E731", # Do not assign a lambda expression (we use them on purpose) "E741", # Ambiguous variable name "UP012", # "utf-8" is on purpose ] extend-exclude = [ "tests/messages/data", ] [tool.ruff.per-file-ignores] "scripts/import_cldr.py" = ["E402"] babel-2.14.0/tox.ini0000644000175000017500000000145114536056757013527 0ustar nileshnilesh[tox] isolated_build = true envlist = py{37,38,39,310,311,312} pypy3 py{37,38}-pytz py{311,312}-setuptools [testenv] extras = dev deps = {env:BABEL_TOX_EXTRA_DEPS:} backports.zoneinfo;python_version<"3.9" tzdata;sys_platform == 'win32' pytz: pytz setuptools: setuptools allowlist_externals = make commands = make clean-cldr test setenv = PYTEST_FLAGS=--cov=babel --cov-report=xml:{env:COVERAGE_XML_PATH:.coverage_cache}/coverage.{envname}.xml BABEL_TOX_INI_DIR={toxinidir} passenv = BABEL_* PYTEST_* PYTHON_* # To let pytest-github-actions-annotate-failures know it's running in GitHub Actions: GITHUB_ACTIONS [gh-actions] python = pypy3: pypy3 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 babel-2.14.0/docs/0000755000175000017500000000000014536056757013143 5ustar nileshnileshbabel-2.14.0/docs/_templates/0000755000175000017500000000000014536056757015300 5ustar nileshnileshbabel-2.14.0/docs/_templates/sidebar-links.html0000644000175000017500000000110614536056757020713 0ustar nileshnilesh

Other Formats

You can download the documentation in other formats as well:

Useful Links

babel-2.14.0/docs/_templates/sidebar-logo.html0000644000175000017500000000021614536056757020534 0ustar nileshnilesh babel-2.14.0/docs/_templates/sidebar-about.html0000644000175000017500000000014514536056757020707 0ustar nileshnilesh

About

Babel is a collection of tools for internationalizing Python applications.

babel-2.14.0/docs/changelog.rst0000644000175000017500000000003414536056757015621 0ustar nileshnilesh.. include:: ../CHANGES.rst babel-2.14.0/docs/requirements.txt0000644000175000017500000000001614536056757016424 0ustar nileshnileshSphinx~=5.3.0 babel-2.14.0/docs/conf.py0000644000175000017500000002023014536056757014437 0ustar nileshnilesh# # Babel documentation build configuration file, created by # sphinx-quickstart on Wed Jul 3 17:53:01 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) sys.path.append(os.path.abspath('_themes')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.extlinks'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Babel' copyright = '2023, The Babel Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '2.14' # The full version, including alpha/beta/rc tags. release = '2.14.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'babel' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': ['sidebar-about.html', 'localtoc.html', 'sidebar-links.html', 'searchbox.html'], '**': ['sidebar-logo.html', 'localtoc.html', 'relations.html', 'searchbox.html'], } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Babeldoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Needed for unicode symbol conversion. 'fontpkg': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Babel.tex', 'Babel Documentation', 'The Babel Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = '_static/logo.png' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'babel', 'Babel Documentation', ['The Babel Team'], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index_', 'Babel', 'Babel Documentation', 'The Babel Team', 'Babel', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' intersphinx_mapping = { 'https://docs.python.org/3/': None, } extlinks = { 'gh': ('https://github.com/python-babel/babel/issues/%s', '#%s'), 'trac': ('http://babel.edgewall.org/ticket/%s', 'ticket #%s'), } babel-2.14.0/docs/_themes/0000755000175000017500000000000014536056757014567 5ustar nileshnileshbabel-2.14.0/docs/_themes/README0000644000175000017500000000210514536056757015445 0ustar nileshnileshFlask Sphinx Styles =================== This repository contains sphinx styles for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' The following themes exist: - 'flask' - the standard flask documentation theme for large projects - 'flask_small' - small one-page theme. Intended to be used by very small addon libraries for flask. The following options exist for the flask_small theme: [options] index_logo = '' filename of a picture in _static to be used as replacement for the h1 in the index.rst file. index_logo_height = 120px height of the index logo github_fork = '' repository name on github for the "fork me" badge babel-2.14.0/docs/_themes/LICENSE0000644000175000017500000000337514536056757015604 0ustar nileshnileshCopyright (c) 2010 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. We kindly ask you to only use these themes in an unmodified manner just for Flask and Flask-related products, not for unrelated projects. If you like the visual style and want to use it for your own projects, please consider making some larger changes to the themes (such as changing font faces, sizes, colors or margins). THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. babel-2.14.0/docs/_themes/babel/0000755000175000017500000000000014536056757015634 5ustar nileshnileshbabel-2.14.0/docs/_themes/babel/layout.html0000644000175000017500000000165114536056757020042 0ustar nileshnilesh{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {%- block footer %} {% if pagename == 'index' %}
{% endif %} {%- endblock %} babel-2.14.0/docs/_themes/babel/static/0000755000175000017500000000000014536056757017123 5ustar nileshnileshbabel-2.14.0/docs/_themes/babel/static/small_babel.css0000644000175000017500000000172014536056757022072 0ustar nileshnilesh/* * small_babel.css_t * ~~~~~~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: white; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: #aaa; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } babel-2.14.0/docs/_themes/babel/static/babel.css_t0000644000175000017500000001546414536056757021237 0ustar nileshnilesh/* * babel.css_t * ~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} {% set text_font = "Verdana, Helvetica, sans-serif" %} {% set title_font = "'Bree Serif', Georgia, serif" %} {% set link_color = '#b00' %} {% set hover_color = '#FC5E1E' %} @import url("basic.css"); @import url(http://fonts.googleapis.com/css?family=Bree+Serif); /* -- page layout ----------------------------------------------------------- */ body { font-family: {{ text_font }}; font-size: 15px; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 0 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } div.related { display: none; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 10px; } div.sphinxsidebarwrapper p.logo { padding: 0 0 20px 0; margin: 0; text-align: center; } div.sphinxsidebarwrapper img.logo { margin-bottom: 20px; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: {{ title_font }}; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: {{ text_font }}; font-size: 1em; } div.sphinxsidebar #searchbox input[type="text"] { width: 140px; } /* -- body styles ----------------------------------------------------------- */ a { color: {{ link_color }}; text-decoration: underline; } a:hover { color: {{ hover_color }}; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: {{ title_font }}; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; margin-bottom: 50px; } {% endif %} div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: {{ title_font }}; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; background: #fdfdfd; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted {{ link_color }}; } a.reference:hover { border-bottom: 1px solid {{ hover_color }}; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted {{ link_color }}; } a.footnote-reference:hover { border-bottom: 1px solid {{ hover_color }}; } a:hover tt { background: #EEE; } babel-2.14.0/docs/_themes/babel/relations.html0000644000175000017500000000111614536056757020521 0ustar nileshnilesh

Related Topics

babel-2.14.0/docs/_themes/babel/theme.conf0000644000175000017500000000017114536056757017604 0ustar nileshnilesh[theme] inherit = basic stylesheet = babel.css [options] index_logo = 'logo.png' index_logo_height = 190px touch_icon = babel-2.14.0/docs/cmdline.rst0000644000175000017500000002377614536056757015327 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- .. _cmdline: ====================== Command-Line Interface ====================== Babel includes a command-line interface for working with message catalogs, similar to the various GNU ``gettext`` tools commonly available on Linux/Unix systems. When properly installed, Babel provides a script called ``pybabel``:: $ pybabel --help Usage: pybabel command [options] [args] Options: --version show program's version number and exit -h, --help show this help message and exit --list-locales print all known locales and exit -v, --verbose print as much as possible -q, --quiet print as little as possible commands: compile compile message catalogs to MO files extract extract messages from source files and generate a POT file init create new message catalogs from a POT file update update existing message catalogs from a POT file The ``pybabel`` script provides a number of sub-commands that do the actual work. Those sub-commands are described below. compile ======= The ``compile`` sub-command can be used to compile translation catalogs into binary MO files:: $ pybabel compile --help Usage: pybabel compile [options] compile message catalogs to MO files Options: -h, --help show this help message and exit -D DOMAIN, --domain=DOMAIN domains of PO files (space separated list, default 'messages') -d DIRECTORY, --directory=DIRECTORY path to base directory containing the catalogs -i INPUT_FILE, --input-file=INPUT_FILE name of the input file -o OUTPUT_FILE, --output-file=OUTPUT_FILE name of the output file (default '//LC_MESSAGES/.mo') -l LOCALE, --locale=LOCALE locale of the catalog to compile -f, --use-fuzzy also include fuzzy translations --statistics print statistics about translations If ``directory`` is specified, but ``output-file`` is not, the default filename of the output file will be:: //LC_MESSAGES/.mo If neither the ``input_file`` nor the ``locale`` option is set, this command looks for all catalog files in the base directory that match the given domain, and compiles each of them to MO files in the same directory. extract ======= The ``extract`` sub-command can be used to extract localizable messages from a collection of source files:: $ pybabel extract --help Usage: pybabel extract [options] extract messages from source files and generate a POT file Options: -h, --help show this help message and exit --charset=CHARSET charset to use in the output file (default "utf-8") -k KEYWORDS, --keywords=KEYWORDS, --keyword=KEYWORDS space-separated list of keywords to look for in addition to the defaults (may be repeated multiple times) --no-default-keywords do not include the default keywords -F MAPPING_FILE, --mapping-file=MAPPING_FILE, --mapping=MAPPING_FILE path to the mapping configuration file --no-location do not include location comments with filename and line number --add-location=ADD_LOCATION location lines format. If it is not given or "full", it generates the lines with both file name and line number. If it is "file", the line number part is omitted. If it is "never", it completely suppresses the lines (same as --no-location). --omit-header do not include msgid "" entry in header -o OUTPUT_FILE, --output-file=OUTPUT_FILE, --output=OUTPUT_FILE name of the output file -w WIDTH, --width=WIDTH set output line width (default 76) --no-wrap do not break long message lines, longer than the output line width, into several lines --sort-output generate sorted output (default False) --sort-by-file sort output by file location (default False) --msgid-bugs-address=MSGID_BUGS_ADDRESS set report address for msgid --copyright-holder=COPYRIGHT_HOLDER set copyright holder in output --project=PROJECT set project name in output --version=VERSION set project version in output -c ADD_COMMENTS, --add-comments=ADD_COMMENTS place comment block with TAG (or those preceding keyword lines) in output file. Separate multiple TAGs with commas(,) -s, --strip-comments, --strip-comment-tags strip the comment TAGs from the comments. --input-dirs=INPUT_DIRS alias for input-paths (does allow files as well as directories). --ignore-dirs=IGNORE_DIRS Patterns for directories to ignore when scanning for messages. Separate multiple patterns with spaces (default ".* ._") --header-comment=HEADER_COMMENT header comment for the catalog The meaning of ``--keyword`` values is as follows: - Pass a simple identifier like ``_`` to extract the first (and only the first) argument of all function calls to ``_``, - To extract other arguments than the first, add a colon and the argument indices separated by commas. For example, the ``dngettext`` function typically expects translatable strings as second and third arguments, so you could pass ``dngettext:2,3``. - Some arguments should not be interpreted as translatable strings, but context strings. For that, append "c" to the argument index. For example: ``pgettext:1c,2``. - In C++ and Python, you may have functions that behave differently depending on how many arguments they take. For this use case, you can add an integer followed by "t" after the colon. In this case, the keyword will only match a function invocation if it has the specified total number of arguments. For example, if you have a function ``foo`` that behaves as ``gettext`` (argument is a message) or ``pgettext`` (arguments are a context and a message) depending on whether it takes one or two arguments, you can pass ``--keyword=foo:1,1t --keyword=foo:1c,2,2t``. The default keywords are equivalent to passing :: --keyword=_ --keyword=gettext --keyword=ngettext:1,2 --keyword=ugettext --keyword=ungettext:1,2 --keyword=dgettext:2 --keyword=dngettext:2,3 --keyword=N_ --keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 init ==== The `init` sub-command creates a new translations catalog based on a PO template file:: $ pybabel init --help Usage: pybabel init [options] create new message catalogs from a POT file Options: -h, --help show this help message and exit -D DOMAIN, --domain=DOMAIN domain of PO file (default 'messages') -i INPUT_FILE, --input-file=INPUT_FILE name of the input file -d OUTPUT_DIR, --output-dir=OUTPUT_DIR path to output directory -o OUTPUT_FILE, --output-file=OUTPUT_FILE name of the output file (default '//LC_MESSAGES/.po') -l LOCALE, --locale=LOCALE locale for the new localized catalog -w WIDTH, --width=WIDTH set output line width (default 76) --no-wrap do not break long message lines, longer than the output line width, into several lines update ====== The `update` sub-command updates an existing new translations catalog based on a PO template file:: $ pybabel update --help Usage: pybabel update [options] update existing message catalogs from a POT file Options: -h, --help show this help message and exit -D DOMAIN, --domain=DOMAIN domain of PO file (default 'messages') -i INPUT_FILE, --input-file=INPUT_FILE name of the input file -d OUTPUT_DIR, --output-dir=OUTPUT_DIR path to base directory containing the catalogs -o OUTPUT_FILE, --output-file=OUTPUT_FILE name of the output file (default '//LC_MESSAGES/.po') --omit-header do not include msgid entry in header -l LOCALE, --locale=LOCALE locale of the catalog to compile -w WIDTH, --width=WIDTH set output line width (default 76) --no-wrap do not break long message lines, longer than the output line width, into several lines --ignore-obsolete whether to omit obsolete messages from the output --init-missing if any output files are missing, initialize them first -N, --no-fuzzy-matching do not use fuzzy matching --update-header-comment update target header comment --previous keep previous msgids of translated messages If ``output_dir`` is specified, but ``output-file`` is not, the default filename of the output file will be:: //LC_MESSAGES/.mo If neither the ``output_file`` nor the ``locale`` option is set, this command looks for all catalog files in the base directory that match the given domain, and updates each of them. babel-2.14.0/docs/_static/0000755000175000017500000000000014536056757014571 5ustar nileshnileshbabel-2.14.0/docs/_static/logo_small.png0000644000175000017500000001374614536056757017442 0ustar nileshnileshPNG  IHDRU,\sBITOPLTE̽333Žsss[``333Ž{{{sssBBB333ť{{{fff333{{{sssfffRRROONJJJ333{{{sssfffZZZRRROONJJJ:::333{{{sssWWV333{{{sssfff333))){{{sssfffZZZ333{{{sssfff[``RRR333{{{sssfffOON333))){{{sssfff[``ZZZRRR333)))!!!{{{sssfff[``ZZZRRROONJJJ:::333{{{sssfffWWVRRR333)))!!!Ž{{{sssfff[``ZZZRYYWWV[RRRRRMSS_LLOONRMMJJJ`DDZ@@l::b;;BBB]99:::m))|""333})))!!!dItRNS""""""""""""""""3333333333333DDDDDDDDDDUUUUUUUUUUUUUUUffffffffffffffwwwwwwwwwwwvȏ pHYs!tEXtSoftwareMacromedia Fireworks 4.0&'uIDATx[#UvJWJR(< lDh l1lql 5MPja#ђ-$ےFsgvvàv?sgF#Y^;ўj漾shatyշԵO'ٟOK j a_~yv&?*cL34M7 @Bl_;oN7>c*㺮sDc*3kڃ@78oo*w0>dF[e%fs:-0 w$S +t6N,+eǶ@lF WZ@VJ;?K %vZZ7;z딋d0+?k/լG+?%ީGtk G?Hrcb=+ E9վ B)cA=9!1O}>*gӠX~,cV8!Y*A.yJ:#da$!+ݓOAQք3ru$@`³ѹ4!n+y>.eCjgzsXQgCHVeBrQ(z-b0t,!Uŕ#:Dˤ:Ht Cjb `$hNJ=ρpc|!` ?S4a,)GVtW~k,;3XROv5 -T#k2OEXLa@=-S$d"x:%3u:&$\.M'tsd`#Ϯ۲.ÞvJbn=.^rqմKsJǶJN. ab=Euam^KJϝ-~`t1ΝfdC648*K|z b5:5ɸbdG 3D6&Gl\1ZdRV(tdDGway')d@(c={X[='U!@1}AMg!WZ/Fl4_i5>߯g񬋑Cal\;#(~SC۶XQ .Yqi "@H >t|-תFy: LsN%+١trQRPٳM xv7W=r{ɴF.fbH[² 99ȥr:[yїrtWL'zv!hÑq@7_4r &͗ o[xBzPg"^wѷc+YcH+F*48DPWI0ݤx02`()bQ?g' $%+MēlUOC0@ _pveC.KZlQ=!QJ5CVฤ倖鏾A$s;n2=90R*#[׏7?Y ~Ja8* +ƂlH(w~A.:hSz  QՕXPMi L5p$}ʚ `X1U.  A%$(@E @ĄQ%{Dd&7Ǝā!{߳ w]58? Ԁk.bPٲMZnfB"|o>䘂pV%L律a ~ĭuz rs,^bލQGB>ٌ :wCIS;!U(L#ufj*&(+Ȭ(A%ʑ8zWnAe)Cΐ`9:śBD{*(i~:8<`{T+O,S0F p 8f<_ڄ6o,ɳnLfuڸ'X{ od6Hͭ%|)=87""ѩAjuj(t1ҪFR =Pwjy ~;zťmt/2hFpfo-/nڰx6 ,d2KWf@s.o|3kڅdu`+UHT6j6mKV UW[p6f9zKaRf~ >YTB?A8UmHFz=լZ]lh!P4O}wsdong65.m jQ孭 9%Ff%\;k=uJo|PYM iLrUjМpVs;L/$<yYAbuM.nZ pG M֗WH^03p/MD~/e^}|*"{|呟 jŸXL^ae1oRn{eFV0g9IYJ[n|^`:"АF}/s'߽k1ysfAB8"ɇ1O&bFt?GZ[r/\mw|ߣ> stream xڭqe_َ=,/$}L-V (EJTA{r=w_V#?k?yͯ-?5)_~퓻?G~|2f+?V*c5zeR˩?Ju]K}pUkԙH嫕\cSZyg{UκN?c eE k-1%mhIKEկsЯg}^T{>Fu6[ԝkў=yzucZY/UQw7YF=C{ciEyUt@U׹f>ڰ1տZyt92jj+K (ڝw^T+V|,mwky@j8=NhoPVi9Deyդ0|L- qk_hc{zMZ>oWK5Nډ mQ~s`7*,!wDT俲.l8apvmk blo;Bmf% :9N,JmpR$A'  GbCdHJ,jX‡_^[ax%GY 4kMY{wZo;Iq* Mݲ1˫4V6=gEhY'"zIm |iw +RvrSVcfKE(m$+OObZ&d(29xKԫ7C)QXP5x*.lc e Zœ>wL?4X6d^q™E{ (S@3#@I9ЃLL\U0]Zmذ/`;-PZ[Yr{rMWy :Y} q$nL=DµQ~Ilݨ:~!- UCށO!m x&u|hH-RNPYW70_ >qtfLj^0n$] Ug%ooF;:9!0Ha9ݑv ϱ"4HX v\dh=F,MH~@CN_`ey7#eAmk܂, `;#NAu ՕH'-(]γO*C#YS1Q ]bUlmud,y&Ȭ}X \iŦq*9SQj/+&*W M&hD)M Jf,u%j< AHE %)ryv&#{`륂$[2v= q5 ;-6qшwwbuYB1dI(`$̯-Γg.lkY!w[j,`<ڍbtOe1'>gH~49ǖʹ؋DZJ^$R`n'[!xAiLiK+ g[0Qg#mnDޮ$'s^bcuy1H|R!N\NZefwHNi"wt9$c 7%G]־`EY&Ѧ(͑Uy woE;tolv%KGK6 b-rbqz:"hbl۶ad w xڕ4Kx 5'&Ze{Oá62,ui`  ir7E;9En1Ƌ O,"Yn?8*ƕƳ?x3si'h&+}VzRN y8H2K|"K3SV@E)M &O"'hXU&i2Y^%buE7"K2-vFBCuab2Q%`hz`1j=i:#6"kiYC$;9 nmJ(,߹Fd%PőP6kb8G[h5<Ǐ;ikXGSaDcЖN0Ch$qGVB=_RT6JG(hZWnQQK3fr`b#wH¦62kg4uvh E쎀nzi/2<d g 'Q/驰> )?$ ?U|^oM`~6l͎+%80-!/':w`xBPtZ' ɺg.lg`Z[:cR'婨DN9o{(PZ '̸nHԃtdʈ1R;'8\u2m,LL682C?C۸^=CNr)ݦl6RBFȿ&)g6v1W(XW xX-L῱aYB(A"-8SY|= 3 Y+<+6l aOC\ 'vAk&OK)ZzM'&팋Ԓ64G9@RXOV) hg_IAJսi`aQ畠4b7{]yH8þH#M6u:⫤sBv˄{0n>OwVxa1E薤Mk[5up?v/n Mh}P;l!K:tO#,n&RWA:} :$W7CF(#P^As)3R2N݈)0aX @M\X;bdǕ<[q-H; =a(AN-8y"d gil#.W_7-ĘWwFhc-xxvDZr( FFʁ i캔WvPW 74l'HH <"*Cz&@.pq#2x !F J'qD{ uU)?8*$Fr!0:;&"dg u (N.^8\efjeŠ,+zɑCH/%pNR[`M;C6eBvw1 aBYEc[Awn|hbAm@WKׯ]݁/Nj+H#s7Q9ULӲtPXpYB8}CKE[~UL`Κ뭻hͥqNGA(N/7]_7): ;CI5r:!N^MmE"?*Wz2UvQ]I:T%i_5:kNCT^9:|TDW}閰8*J+InKSd O, W[**Յ(EIKn.X]Y)2T@f1coFJ4&ﵹ\L_PH,tN@lw#K~S:_X2AڄΈݩS| kP%Mhm/qCLWC7ùw֫ʝ׫Vw5-n"Ezy8(B(]KFpǽ<X$QHX*?jo1V"E./ƲC\ yF$z\I"2v؝4$?"?,Laz* Ȅa~1 MZ)A.,!dVFxgmr5<"emVc8ԋy n_1$Ol 6&ʂQ";= lZ   AvR Mx*\.00q"<#%ݢwdJIA+Cs{lXX.ԉxԁco~wi 5d蠉z (R\Sgd#ge7x"OܯFpmXqWN]ikufE>Z3$T :g=v~tԛV qv B5;ԟ->Vy'mN^$Tkʼ_xcIkθv"#k. [:׫B$E3FZ(ٺP K]nД^U{( '0/;±9aY9| Zb f]J[1˘R+dLJ,(kӹCul {ЉIq؋NEM?/>X.I;%sm68 І*F͂kpⴕ: @V@tPez vwҥaTRhI{5qǗw=~WV.F.GtO_o>,Imlp8%B <3.hyQ^FoPa`ZܮF AFDوdW-FB zvXSTW#x=cz >nYBs :RwLKw~I1ʤuQnX(snI7*+[)۶x˝Yv?[)33K9#Ԉ֣G$bըuq*@d$Jg:hGt2fݰr9"~.Yo tKtRʸԁ>nS SsD\ <>bavl. ZexQ׵PeNtFcr@&ԗ\Kw~]:}^(k3}$Z?ϭZ)6~_̏w$IB; >yQUj\G@z'Q3u7 =nH* ZfvMXmKԕD CSBт}ў>F)R/3;p٫lEftN~Enuhi{)QܳWIJʂ S vۊcbFCJFqmK%_7uq?)nsЉ5lqJ,aܬwO*.uRJ.McFErAW6Ӌ hѭutב2Bs5E|w#er6qͪ5=UUxGYnWY""שD[< 1ڋS OggYRHX;A"O` 7UniBСyXev4{ $xnʮDhյbv(b!ȣtpeZՈjS|fݰEc -+DPqGe"|PXKeBn0 >J5A#ʥ*&qqj=uj/{8PB0]ѣ\_p6&nΞ9$XWꌋc.ˮyڶ-].}BЈR4[8'B]ܙق^VY1`96BU" ,Wu6ޗTӲN N1z]oTکP~0?}^+y4+FǝLᶩ1lrH5DI#h3qjmLAYSAe%kL)P: bfQh4A}&'e.f1=k]+MwRF"4>7 0@ǝn d*aҹ jg:cÊGd \6Şܾ4!ElB5naۖBk(jRuM5dR&Q@N Ȝ}h$VmDK͵΀f%36|IyfHPR@i514Uh'|GzǮo%%.n4T~\}\Fq;7\|BJ{:7&ƺ Em{Eu6\O !$oݎy #З}ߨhpa,TP2d#eE;wbm/h9=m-񶢮;`#%uiv4,ѣ'Kh93I>[ t3DGvCtEpXHEn޴K6Q/Xg5XnDe$ISœn„KCd܍H8 B؅ Rum%y*#JU9/uau?8eUt %mv{LU =N78rjt0 HS2\)>nCB'"nP0 }|-&PVӴvޤp_5ʭm': v?*vu5arEt7IX!^vJFD!hh4NהԲC %ɣi!Jos;p@ȀhQ/"EľA6@3,nQtr@f4HcF%ة>3I\[JK {˸GpȐ`ڊ1tF͖vC\'l H8V -F?dhctO>ZV(uپ54Nq|'= 8,Ԛ3*־}P]3HUIY- rvPȽivn"ු2Rj Iu\+9#:uRal ]rݦA:ť<,zLAL"CpAQzҴM;4${Rbt Ph4^ |\3սAfpA׎5d䴝OϞZp]=C¸l\D^IcJ[Oj N)G;nIdn9D뭨kq t7, p1z.r0-\tP=HZ(W~|63) nrsyu/H=klΗm58%+"f!hҘix/<<},kuZz+N^nٟ+X32c@DWnjQ XF )&I[Jw0y͌Hvk232֘wmdozѲO!Փ{IQ'@q'}SYcźo?ZcPYO0Uh%@EX#]E9(x)[(|T9`bK8=v_$8zpa!rf&I'ZƍB5#:">{B`9-Tnՠ˖UYv d1 A@K챉 <>! FY8J8^+Xq¤n`l;o,i#腦QKdL=!@# u"T6VJvQS᠎~D1= A2Dl"hS S6MՙTv+jߊcR$Suu'q/ :I=AyW@lZ mugFAN"4x~i)f7q .Uk=5}0ZsmLiúyzN_SAk,_i#WJ!XC򣟿@tJ1R Gi{h\ܲ N|"HpѩM }+{y|mjc":~Yt6Ls3:3NPVglD3x/S^Ɖ1~]y{N1Sk9HGem'm5Y#,ǏVX!f*dp1˃hgSwR!Y{'0W04Ӵ xL^%xd=$T.ZpT;|}3nSa20۸UΎhJl Cjp"E&Wr,{+ϨQs+{ Q (7M OqJ`\ c_Iz+Hu >ܞh_`P{8yFQ P~4#T4Ż$¬#bRʹnJiLGuy"譟-+\avܝ([vÚ)Bzcp)MA;@H HQ D@{Gձƛ#7<ݬF-(Xpcó==9M^qV =nҶ<3mә'#i^ kgyD`2+J[@y2c5bN™(ew ]-ৡsf"-qۮ߮Q͐`b%/9Q L_~ /%$dW8I縮$BP-iOЧRy Qco^r W؊ lCd_a tl8T'^yUj:WJkUZ]ZY ob~AOJҽK4R1 5[-nE}Jȕ$C[?c*Cz-D! l3<꠹jq +ьVܼNj3U툡Քtq*1K*66oA;Sו2He,54yVÃ$l@R_ue6O/o7hsz:GlO{| 2{.pfK&CuOt&&{$p'6`͸݄1&PM9a |&͝3@1 pn%3 Ț3 4҄=뢸r9a N8-ry;CKQODf՜k`9{$X_ԃ.\{\8֞S^OSj"cM&Px.m>i`FUF. N߈RP @pǍ](OsA62 jGjݮx:U["M 3l$6m 4ںX4Y7\e{MPTYJ!%5N6*/ g>;ùxox`V)7ZY޼u=8 0o[{x+;Ue*;z<̠Z61q>Y.d;OQ՘s1\N?3D.qm4B@S7_#A;=9jMLk^7ќIr'H jLOK?M%c%^7_nSXجS!n!E,J%HEu8FLpY+{" otD3Q@>\Q\$blN;2N~~BAuڍHz1l>aד{afv~{ByZh tI1)yRCT7D$]Cn;c#2KM|!湗qWnBBPEZ4=6a7jv]\i]EPbh#^]U68.iA8e& & z1@ydxQ@2v )ɽs]iL:|UhF.g]+[vq'N֢yE2*_#;ZjAkxlsR44qЅ 4_21vR:-ɶ$Nsʾlњ4/2`>TP]w^:]Bm^bLW.;}@ Є[i֮ЀDN)7= ZZ#ɘ= "Rh1]_3=o*#9nǘc{3jGhy\062іd`Y@<7D0FrF|nSotp =n4lkR~ܯ[V03_ \v:>Q3gOM3@Y~+G=K^,RY RԙD9Q6|C DnuD^1JuӻGN#;Agj0e d˘l ;Gͼ?PrqЭhFgKeL@ s7mQNһ/x~7E.oz_hvluG Ẑ{ k'1A{8f #h0]Ͱ|+mT2km{$[M,,®h#[\3^$j =9WA'%'o?a{tۜX=ɑ羅j;ЪcڳW$؍d?e7΂㡃ݙNM>㵷%݆7sҲVX3)Gl@w 7Z8vʷ+)k[̺2mGsЎ,*0{[QeyeDQEr Za7i|K|W/ɶ5'Nhu:"FBTy^a ZoiwD8\v. {anGk]yLb^hzלAq dW漣uԼY =ri]/amw0>]_^lۡt.G2x:c[@}{=gWz?7=;֮ǿ轏l_7n$P={([a>vtN ׿]_FguQO=:Ve]s:}ޒ$@YD1j_㾾K)1ߔ\/\Lƻlf=_݈㧞E F/yI~ x%Ob?X1_JYcv3:Z{A!wyjhvWŏkA3׳?(/8@? s|?+:#a&\: vVZٟgP endstream endobj 4 0 obj 15951 endobj 1 0 obj << /Type /Page /Parent 11 0 R /Resources 3 0 R /Contents 2 0 R /MediaBox [0 0 452.20737 187.99998] >> endobj 3 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs2 8 0 R /Cs1 7 0 R >> /ExtGState << /Gs1 12 0 R >> /Font << /F1.0 9 0 R /F1.1 10 0 R >> /XObject << /Im1 5 0 R >> >> endobj 5 0 obj << /Length 6 0 R /Type /XObject /Subtype /Image /Width 242 /Height 188 /ColorSpace 8 0 R /Interpolate true /SMask 13 0 R /BitsPerComponent 8 /Filter /FlateDecode >> stream xk]qdF AbFb$FKr'va;\BH22VvN9Ϝf4AA5:}]~{Ŋag=Ss}gn^s5_=_~ƍGuUV]q5kx m1ȱ_+rbb7p kΝ`L]uU[oرcW^yo}ƿlիW>G†`X m$u]w7~{ߛ'.|{W[ op@09^|huu-] G%vf]&?#U'N@wG+TcAi@3+J MPAW$V{{?.{WjB`$Nu@:np&^,9o %lw/߈F+ftpu8 qS8T8-DyGشiөSƆRYկ~_=ztjj*d@e߾}{sg}6\I$ ߭IEg# c?yXz֣p3~O? O?XP8fGjqdh Hoyc D/zICp2Zq@lz@G (x%+y+V/˸YZM ƖٱcÇ#2 y{%~ZUlڲeK\|j `|ӧOzLG c,v5?1.G) J7?NKSl"W\qY9Y@E:; ٸSkǁtsf ^oԗ-Yb8ֈ/}6E~ӟ$"l">s̀Eo6( ll5>YcFS ׎oaqPA3pL&@49XjWWWw;n$Gah 3-k֦x;7 vGܘ0#{챸cH0e$Z+ݤ&LsYT6`kMYE1.vэP3GBQ6x 6>|>JGdw ;_~g}6No>T &9*q:3q.c "dWWpcz6gsj3u+kĈ%9qׯ_kK>Cs\6l #o]f Ȯ_Xz%_&̹4]7>ƿNqz:x+ X4SSS "~0 ]`wAh@yB)=Nh23qGԖE_]o}yn- bK9 ~=ݭcww 16UX槬!J{?~|ݺuZZx_ɿ[!%%yY$ʼKơ@%p?˼oۇ vVNMWG ~2CNݺF)Gxvh hr|RXxѢcaׁi =:| n_+5&Eû,ɶyI>w+שZQ0*y@r&`O~bwc[udn/吏zjq8-peÊJ#8demBgJq[3^O&40*~a TKV}TyMEο(wo[guϸ#4KM [n/OFcӦM~-7;]T7p8,zZ'W`fpSkzפD3Ò"`s}֫9s/NEPF KGޜ%^nܸqq£>焠F3H9Vqb o7kU\][?rհVL5+Qz⛸J[l!b؉d$GWmq^ovj|jMSh^͞lxP xg }:qj6Ch÷ ` 8T/pu ]=^+fiu}@kzj~EE_V(5_g;y/5<w޼ys=wر'NDLKB^U(@s3;~|\LHYU/)8`3(l ud7mUȤ,. 3`^>Rܗ `\-[ d [!]iэHTJKAeSl-WTUZ]ȥ:011VV&_" V7B7TjQ^ڣB؞bs]kQH-s.7/'atV4W\=tn- ?o7o5sqGUάSkgfju)!\:1!xH %kOCZkRy~,vfV{*:h]x s`bN.wod/K/?~ԩ:(4Bw )gXPеj"g[lR-Jd{oEDSV7{QMb*o7}{m3h; ;v[o-?T%`n%6/>ƹSX4BW 1ba`6_+(xV9k{B?[8Wف ^)nǜ޸|.?T=zȎ;\a갾D7Mib S䫡 .+:!@iw5Ga 3UY1 d=11\]ܰ؈xs]ql*, lil8.<3ڥQ7Yi+z<&<U{={r/ƻ*P%P5JjUSmuhtv캕Z vjx%4d]`kw\sZ)LZy+хȔ[υRͮp *Rj)4Wn{bl6'[>R," `{YZO-S>v7jYuqTAy`oM=weu*̶&}EK7ي|Ch e(ETVr+bbYTnǒǞ YD(V&t`&U+ׯxY _c-+͑Bn(A?bExKnCT - 5@,Sa ܄X^K}p3mO;R! RoT3u 0X+e.dl& }:Y@3 ),)J hsyW61C-H41'`)N (s2]̨P0֘"AD9dm Y'GnC Ts\,Khxx4"S` EW+yH u^BY̬V_^*>}t\},̜{¶~DN+câ0ӷ[Do…#5d<.Y]W* 5c+ h;Nxjغ u9fw~uרΚ*Ymuk֤6o޼$ @ڱcG|y:&3THj9%C!Ǭia9oOPszOHzCMPW<~lpK+W>THsg1 B{a8-EO:xκdNI܌(q< +qPSܳ_$? DawB8Xid@ƪSDŽD{Vsr|l.{: e.sԲI\5k6kNS1pnSK3t>.&{BZ$ y"`91o6p*dj"I\A}WWe0겟5xw.wvUѥ<9DCSx1[lTЧ8?9q!0~}H/8.4ĝw LCžqlCH YCLqvL쮹œ/t ,TnILpM;9Tf.˺4XQIi7A%ECADP!%Qnw^[2ˁ;j)uH1-6mayA;-8m0yiI?[Ʋ6Oߚ͸YyIE#TW)Mf]S J/:w!2D5G'>kM0C4rbH?b4CKǘ1qbF;xKwnuN-l8h6 Oh x wc ep"GL+XȪe;asS%Ve&+ EyѕlUu1yW ԓ[̝_3W&h&:p'G 7*bkb5k-zqEWWcà5 MSxF+UJ>߬EYm+]Mez| Īzh [dѣկmxuō T`~s8[1z<7>NŧY]Ade1o=uq.RàgEWqj߹Vf9Վv]NW9v&UWv1СCP4CicM#؏,P| ?%vfϡ֥Q+ғ׫]~or EY0]OtYj\E ;vc`kMLg $%EAu 5}Sgeik :✨`{ns%. Y}>M]t!7t.{' 䕞(h80vdq܎T#MݵZ4J6[%v@M3VC^fDw*zn b{ {ƟNX,I}/ .1PPT㇘˃ mV/HT|cg *مD@h׃UV/uxeTyJ(,奛m #j%cNsh\i@D]FM 33ϪUSQdl*zbueͨTPDՖ M1͘18MjLZ OSJY Dx$juJ,}VɭdYy6 |[l^lވ*vpFSrMdVs3יl^n-69P'MnzyJ/6NOGkN] x/ c8pHS VFH/ B7>=iH\Jfb^7fW9hs189B_}$?ZSTS\M=kd[C.C=?נ {ur4wΝf/V@ʤ)%c mA*0P'j@(FYͪYfY+w"XZ Z䊛<qA 3(ڇ'iOݡC-NT8 wt!3+UҔR%lMfxDF"8f7E<=# #΅-EwdBsxB牨ʛ'4| rYAl3 Q9amPLC9yu?y*Pm bI 01# v@>|yv3HNÄ 8p$e̞g^ʼ)+l>()|,TnuLYܢҬn1;d"F9ԏ67sYi =,/Iq=eƝ -{-9{&E< ONNNMMmٲeFK!); *V:3F:v;MH1泔|W r#A>{DPP#q_%Z=gZQDVS3LI+ǷT:JWT'yX-=--r(,*.[Hf0+ni{SY\G_=͖FS%*~C 9gc]9Yq4qp8'DtCh(c@y*7tbI4D "ϼڃղbuy|l.r)GB1w5M35]XlW=R32ի( "{:wӲj4Fd@I_̍/RXfYjՄB WtdQf1juzHvwsarE9uY[Wzx{% 5*g>[# &i*DX||vnMi\apR: ˦8|ks42Tj(YдrQ1vB:nZԚ\?3iGj75Ӽ3h]Z2](XSȿNJXl 1;999mD[7n el" ΢ͮ9\:O^v\@dV҉y1/9'h\Ta|D#c87X{<9Xqf~\H-0ؓ@+bIm۶bɶnu}jj*W/ GE 3-SRvKIr"dcfQ {,զ6LP3+!`N'O9wy/PN3RmJ)^υi6SEl4:\eu8\ߦ iP/M!Tvsj2i7$^o^724f9t N`k+ otㄙYL5yʏH` :WX\ܧz$X^ɓ$R0.*[LJ`fKQTggoCjLբ4Qbwź*t!S݌fGss,_|ݻVcS"tupw-R x[zjdQK7eHB(90Lm])YăW[jW):?VP;<8f<ǼǑyBE-Ʃ%s8?+{mݺ&p[ٵty9C7P!I]Tcd4_u"w&ם5)C:,r^ayfٳ"Uvffɖ豔bA.ЫcvI"{޽q]q ~x׮]NBjl;\ . u[s.)0.h싢h[=%6'`KN,j4tbJEٛL"zSN4kd~@5"y7|qP Ga@g'VBDRFK"3^4͖p Wӌ{n]QU7[23>ݬ㛳+/dQGt)bVs6%1Kq_|155e#[quh9NB2ExPN@5ji!>ʋ<cj1%+͉I$UqzO454u K ~a6|Unuf-[&WI]%*Xݙb@%1$Hͻ꫻Ȝ\9}XsU+~g!Ȼ#X#t7S~DK?.vj9r,O"ÇH Y_a- 7ȑ݃L5Cbb2yIأ\4/{' 3t]O믿~VeP"B:89oby=ؐw%v1Ϙ{Wo<<R V[o+ c}|J&2Ӻ"m~ϩnhk,YN꜎@2̞JeҔUC#[0h-~l ɝ=@Q3i=+ZKA;߭YYEwPW`;ܸe^ 6Cs7R$jV0XQuK _}_Po sH?/ w 6NLs/9_g6I,8|1>RHց9I=E&v:j;׊|J5mRgfJYPsĿgΜ}(kih e^gw GyΉTzЌ,ު$=nxxmY|O 'MsPȘK'͹ bU$t쯬k(T'h,Mm7O>ds^;g o߾]4<7FBoCs:/ % ]yt8 r]Ĕ Llm3;B3k x67bsF*AkRO^=wjz.K,OŅSz=hX]uUDHrCYcTsUb+D!1|sW(Pf9ۡrIjf @{a ĦvSWWvƧ駟ۿy>̙3O&mk2gBaɌg>9 q,*pM(e6N>uWԻ:2ɱBZ攟Mjځ7|ST-sӗ^W_y%uV 05Q8M&"NRDkFl Ts)![s{f׌D;ɍZauV*zKd# uҎp)R`T!=#9clRNh#p瞎8橈3?==ꫯ~GŚarCgVhoҧ(:i=yj _*A+ޢ$޽{-6";iwIMHokx@60شiͱyRg ޾}{aFN 2NڼT&lɍ EIH-k܁lW=c-|-053&c>nFD?[/e0pm655qF%٩F];a<6am8;0~E.fMUM%5f4;I:^E6Rkċ_ =\ 6mڲe S Gh.phOrS,0kN&#>C,9..{-YW-mRC6"f%1fA,ff5}MEF>gHOF&thٺuk<ЋÀ[D?)y3s*0rLBs`v@5٨^kQ=gu1g$#6^) kW!ьZ6(}lqnTQ^gaV5{w=s~ѣcVKRI5 #5`!mbL"}_fBEPUz<y7R [*HB?v.] )fS2TXk`5l&z4Yko`w#@x Fzv<5I!#f=&X&5]StP&D=Ms)511{ͼصjMi$r}i+'UTDY_/j>pk$ NtUS5u>55eԏ̈́[~W|’v&.T:o!4{5*sBEP^?p֡Cn}p-`N# R76Xr>v o؜ɚ w2rV8g}{Ν;@ F#ҹcpWlx5O# (s=u} ѦҁzEɘL[='A77UբR"ֿSzz=3rV\lڴis3.lق$#YgUPb>D:D0n[֋mI)ͿԐ(PI &T[Ņǐ_Go!jhFRr#3Ox17>y]?{e=#af"޽{\Qvs4k0Ũac`9c!Ĭ%l/66kܱS*["^1O'sVYF"l'lj74'hb/n؅_ch Pر#@; u;#Ϸqc:^ r*J]@ Hq IQquTTR5ߔ- y񋚚$.]§]]^$ 9w|0xa0t>23{Ee5E<8 fet=QML|JI,{O@Y_u2(\e==hHN;J /GJI^\Lf]98H0saa:ᄄ#}m8p "2LZ,l5IYdzʆxwnC3amIuD=E[".J[ss<-u̙Ri zCGAXrL.[߳F|ZLq&u͂ϦMd]i Sӟt@RY/b /<d@:bI|s \8%iR@F3AbWFu)x71hqѣGӆV)={P8gx10vj&M Jxf_=y/npчXYsnS,3{?yZ }v4TI,"hsNB? e 1TzWU EU ^ȀYq ֟"fOhJ9Bz(aZZkrr2~#=2Pr m 9 ZәK#x eq4Fx8.M;`IƖ.Tky-CmǎвT9V Y9ݻw8qrvn~,ffnE[%0ZʚQ5>j@:q<9W-̍8趠﮺jMhq axɻwTk׮~={Ł5i2q)l<^SvھG]?ڳ,u/=Eu,zVSURu9Pt$e]n 3PV;ACdVcC}FEe jخ/SX}9c>M)0>UaܷXx&? YW3<6 qX/}ɑS=ӷ/P#loXfa&Ⱦ+EP`ɳ߫#R܌TMg+!>q~/K/to8;v.l3A,M bH-Հ3(45b_0:~_~?O?4N_N 6;<3xxڈ~xhNue{r 8ڀe gcbb@c"9 Hm$G^< =j "gjt??9q"F;e,jfhPe˖lqW(v"08~(B5yXSwjU•M&j|`s=\'>߿{ng♄O?Irt4ALKGRVZ`VǦDMUEhYoBx?? 0Xf'D/4I!5C"GpZ T#_m~JPqgUW0aÚm߾FaT6$T Pe}(<F2鍐-un1%u|%׋/0,$M[n Dk)C?#j{P;s7H *CǘL=\v$٦){_G "Jƙ{GkϞ=(ޅS Q#{4'"Ja7*b{sLƀKs=4bxaQ! zT %Hq׍1<^eb-TYN_ȑ#am8BɪC`d)mjƁ\PXTǞ.%nGL{L%`/`4%'wj[$lq暚.%>cԜɰ?I)L,$<3G~xp&zy 4x|UN@rnY<L1*`fB SQh"p) /pĉx PD|2x@ XvnQ/SM*),IGcc TjVG+߿ʐÇ;vN.PʓE˨ƞSԄ)Fc3|r.`pHpQL3DEC^j3&\/7$ z߾};QI%Ѝun Fae"JذgT`\XVo 4믇 G&S>0!XZ OBx CW}g@⍱b?AiXsZ+G<NbF,r8mx̆da*裏0B/S/r+|JX%?#934vSCY~Z aoW_}_G>3?Oz뭏??·qԩ>h8ְzb endstream endobj 6 0 obj 18977 endobj 13 0 obj << /Length 14 0 R /Type /XObject /Subtype /Image /Width 242 /Height 188 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream x=n@ T m A<p#L'ui$7HIe RW 0pDR,Q"H"w3ueAfv.^ r?v/(R K '$a9L/n$c6)eDGO9zfٶ )ty-\GÕRȎcFx±3*pדR #Tل{OFY=bY  Ef 0O| 'f41s#c ӌqI5fq0f"ϊ"oK"ix8YQ^Ȍ&~'feqpe.`b9XN&6, 'Ov?Y5u۶6%:k/S‰M|R9M,g_s3ZɋM]ƉoKkjۑX{WYW‰X%?zm@PcX 6kQojUE |u;=ԍ|y& 6J{he{81\P[cbzKt? ljJ.DQTBU0:Sy) ﲒR(R Xg,ɋ&Nn61@[y+]x`!KsHk{ϩ &3d6Mlbuu?7=KZ/$*05}&7q)Ll.6M]<ī2w2L~7G)t)\ĭR cSMlny3)wI: mbXN!oX!M;ّƠy9&0]O܇klޗMwXEƻo" endstream endobj 14 0 obj 843 endobj 12 0 obj << /Type /ExtGState /CA 0.80000001 >> endobj 15 0 obj << /Length 16 0 R /N 3 /Alternate /DeviceRGB /Filter /FlateDecode >> stream x}OHQǿ%Be&RNW`oʶkξn%B.A1XI:b]"(73ڃ73{@](mzy(;>7PA+Xf$vlqd}䜛] UƬxiO:bM1Wg>q[ 2M'"()Y'ld4䗉2'&Sg^}8&w֚, \V:kݤ;iR;;\u?V\\C9u(JI]BSs_ QP5Fz׋G%t{3qWD0vz \}\$um+٬C;X9:Y^gB,\ACioci]g(L;z9AnI ꭰ4Iݠx#{zwAj}΅Q=8m (o{1cd5Ugҷtlaȱi"\.5汔^8tph0k!~D Thd6챖:>f&mxA4L&%kiĔ?Cqոm&/By#Ց%i'W:XlErr'=_ܗ)i7Ҭ,F|Nٮͯ6rm^ UHW5;?Ͱh endstream endobj 16 0 obj 706 endobj 8 0 obj [ /ICCBased 15 0 R ] endobj 17 0 obj << /Length 18 0 R /N 1 /Alternate /DeviceGray /Filter /FlateDecode >> stream xڕSMkQ3V܄*tE0~,icAǙd$U7nDn@.pQFQ(_~(t)*ޙνuuMY/ Lj;dTs3Bmtޒ۩ ֹ|]HROk,px\sEf?!:t|04VjU0̸ֿзaK#kIzE~uܞ0-9R쌢eLc VtBr=MQqN`<9't_aX 1/ofy!%)_N/V}*8')3 oծf-G~;$Nmb4G* "ء *,A/fET္06uLIe]z*n_/<`UQf ǝOcˤ&p BJQpxG {mj {Xu>9}+uC=C2nGjZ٘W{~};o6z8lÕ33rlۉjQ endstream endobj 18 0 obj 631 endobj 7 0 obj [ /ICCBased 17 0 R ] endobj 11 0 obj << /Type /Pages /MediaBox [0 0 612 792] /Count 1 /Kids [ 1 0 R ] >> endobj 19 0 obj << /Type /Catalog /Pages 11 0 R /Version /1.4 >> endobj 20 0 obj << /Length 21 0 R /Length1 3848 /Filter /FlateDecode >> stream xW{PT}퓽w* ~ APPTC '&M+:cbcMT$i8!5m4Ijt_{{ws~wj 4k[zy"`U7}oֻ;f ]%食~ @qegQ +"dۻrN9]ֶD8QuFR*NΐGaA=iK;|cg0gfW~{kJ'o,tɼsܹ'{{E<0NH]Tt.e#pgh?DSk>}*i zdUGDdY\a8F?r:lKzviBUxϹ99rlZ~%+)uztC}W^;hPkYXtYS:5tۙiumwϽkׄ'X狝=Rfū_C1muk0"En+48V Dū\ 0c=7r}vt:8LhBh9ۇ5 ܜEXfYQ,-^ɢ^ɂeEo6 (EkkbTr222[A51:> э⍊K 2ǂa43kslK9 17M#|>MuQeSX#_FRp\@> UZHl^UifYTˀ(h5a13#V2 AGV5y~`sg1aSTq>YYo9bP {hx,A5 33CfRI p0phH{NKfWTKG֢3+)0CCzxs4k%7֕ke #}stף\+d C Q:u00BYOZ@^wߛ Q[Ur% ?Dɒ"1]֖[T\t' ŞyE~[`qYW:]Hvv؞>ۀƼ`(m> qJ2nsxU3q4CQuL!#gDh!4E157 ҍzkq#ٗ'S>kUNC :$RT7CLUHtr'fb޸69 fv8Å9 I;/.*N%랗V;+%!ǞǶ`Iw$*6P F159;RdME2o,6D]ޤ?oͬ_"q<]U%gtK덂Wdanc~C77Q+y%7{fT‰YhYUw$c׳V6"pw)=Wuon>~E_gxM&V0J @kQaLJ{cO^{lmKBBMG'G-Q`Y5 \JZ ~P_o  dzE盉);Og+j͎~♞Ž~3P3&0P6y^c%$W:z|D!ŗwhw.?i[v?Pw 5|L;X8d9*.3^8GPA' 3P39i'C@C4>Șl<)`)B`>-!ԅ1q`/-KhIi]?v7E)C,*X PĽrk$v`fB+PYff( \EPBVQƲ"V,gU,*gK~5t+!US,zf;ڧ =S|Oiyk/nGM+i~e#6W4N2tMj+5V{x/|_LjB ^|9ܾ;\{q H# ,;OOI4 ~ endstream endobj 21 0 obj 2813 endobj 22 0 obj << /Type /FontDescriptor /Ascent 777 /CapHeight 663 /Descent -223 /Flags 32 /FontBBox [ -500 -393 1351 926 ] /FontName /XEQQWT+Skia-Regular_Regular /ItalicAngle 0 /StemV 0 /MaxWidth 1397 /XHeight 506 /FontFile2 20 0 R >> endobj 23 0 obj [ 250 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 727 514 727 727 727 510 727 551 727 266 727 525 267 727 581 569 555 727 385 407 727 581 727 727 727 535 ] endobj 9 0 obj << /Type /Font /Subtype /TrueType /BaseFont /XEQQWT+Skia-Regular_Regular /FontDescriptor 22 0 R /Widths 23 0 R /FirstChar 32 /LastChar 121 /Encoding /MacRomanEncoding >> endobj 24 0 obj << /Length 25 0 R /Length1 1300 /Filter /FlateDecode >> stream xڕRh[U{&M+pnsݰA#V؏&֮{i҄)͕8+ÁTT K,+cq*@U&y[59|{d v6 %'*7.&׃<`׉oџ>x@_ e )(vQۭHtH{>KU /İ" #^9hL^$2x|*-L]'N <։>Qkj/&uB1[/ Cnt;L#a)V`\3M$ʘf;IR2Eaʢ8n.7(U>Ewq1Q&?%g/`H5p '<s=5 TiZEKsmMUd̗ќdaFJn?N+[7{e7J8NVV㊓yL>!|S~yJMZTO&&K)lЫ{vk%qq>di ]:mkmy$[PE;\ۖ[SGt:+L"f)|M1"[\N77_ž ߙO^< w6K/X{ Dwlou|}-|MP\6$45 ~.y]|WA3ԜӬrA ȊG`V˳b4cY1Y'DTϊCb<%9oB b@V , z>]JhNt؁Gt%SyYo6+%z>b)od endstream endobj 25 0 obj 969 endobj 26 0 obj << /Type /FontDescriptor /Ascent 777 /CapHeight 663 /Descent -223 /Flags 4 /FontBBox [ -500 -393 1351 926 ] /FontName /BJFYKM+Skia-Regular_Regular /ItalicAngle 0 /StemV 0 /MaxWidth 1397 /XHeight 506 /FontFile2 24 0 R >> endobj 27 0 obj [ 415 415 ] endobj 28 0 obj << /Length 29 0 R /Filter /FlateDecode >> stream x]Pj0+dAoFPR>4-u6Z|Wڼ 2Z~>:2LFm% 89 ֙|cYGKߖsmO6ؽ0ξ˱I3 )DzSdz8E-ۡ-"H͵  &'d=Co_$y )EmPJE(Rn ͚R)GgV\ s endstream endobj 29 0 obj 228 endobj 10 0 obj << /Type /Font /Subtype /TrueType /BaseFont /BJFYKM+Skia-Regular_Regular /FontDescriptor 26 0 R /Widths 27 0 R /FirstChar 33 /LastChar 34 /ToUnicode 28 0 R >> endobj 30 0 obj << /CreationDate (D:20070615105026+02'00') /ModDate (D:20070615105026+02'00') /Producer (Mac OS X 10.4.9 Quartz PDFContext) >> endobj xref 0 31 0000000000 00000 n 0000016068 00000 n 0000000022 00000 n 0000016185 00000 n 0000016047 00000 n 0000016386 00000 n 0000035561 00000 n 0000038311 00000 n 0000037520 00000 n 0000042037 00000 n 0000043889 00000 n 0000038347 00000 n 0000036637 00000 n 0000035582 00000 n 0000036617 00000 n 0000036691 00000 n 0000037500 00000 n 0000037556 00000 n 0000038291 00000 n 0000038431 00000 n 0000038496 00000 n 0000041399 00000 n 0000041420 00000 n 0000041657 00000 n 0000042222 00000 n 0000043281 00000 n 0000043301 00000 n 0000043537 00000 n 0000043565 00000 n 0000043869 00000 n 0000044064 00000 n trailer << /Size 31 /Root 19 0 R /Info 30 0 R /ID [ <6ab6ba726755bedf0285a9332bbdd119> <6ab6ba726755bedf0285a9332bbdd119> ] >> startxref 44207 %%EOF babel-2.14.0/docs/_static/logo.png0000644000175000017500000003617414536056757016252 0ustar nileshnileshPNG  IHDR*NsBITOPLTE̽{{{sssmllfffVUURRRBBB333)))!!!{{{sssfffZZZOON333)))Ž{{{sssfffZZZRRR333Ž{{{sssmllfffZZZVUU333{{{mllVUU333{{{fffZZZ333fffZZZ333ŵ333Ž{{{ZZZ333)))̭{{{OON333Ž{{{sssmllfffZZZVUUOONJJJ333{{{sssmllfffZZZ333sssfff:::333)))!!!Ž{{{sssmllfffZZZVUURRROONJJJBBB333)))!!!Ž{{{sssmllfffZZZVUURRROONJJJbBB[AABBBZ:::::u''333{ r##)))!!!ptRNS""""""""""""3333333333333333DDDDDDDDDDDDDDDDDDDDDUUUUUUUUUUUffffffffffwwwww9 pHYs B4!tEXtSoftwareMacromedia Fireworks 4.0&'u IDATx}y|duaz:ݶbYY'FdAdA!– "Vy -3VlGLOg]l۲,P(smc4HS!:Z~oྑw=EVŠT ΂ 'X>sB̼n=#'}e^V8T6*j2a<0@97O:zyZc U+K6̀?!r<1'zI^S1ZQ,EN+*Wg0~I][lqrZf<ϧ +8. I\:heT2!3}I{Ⱥ)~e.5 > _B@G(!HKO^^2:6>==;;F#X2 ZFꔘ>_9G'g|Fi5/:CjKU>uPOٱ?>+zZ +6GEt8x! {Q#ݗL/ؗrNN3F(l6Iʵ#P9 * !ݏQKR&8_Xdo\z2 OHтG%! %P 5v0[(^:FhQXvCAH1רJOaصn8l`> cN%/|450PRNَTNX5Я̝?e$y9 TTUa&]}RxpDqJ(bjXF➗C@ 0i+QlZpP7\X1,'/\Ⱥ355y=L V7#ģx Wu .|;i,q*65BK!Ujym%Zо,&[m H)o9ץoqSЅ ozsi{MtaEA %_PW ٶjJ` ÀRGP>  -̀{}OP̓R)X]-ROuU2bU:a`UKG]zsUQ8 {RP Oy)찱4anY0yK* a)TA5 H)ƆC CLc= Jp (⫀ D&@E={{PxBzb鈴g6ϹV-*hZ>Og>TcCC oV}4]7Tx)T}A51cT\[^bsX3|QD3 ?WHz~OITTq޳8ANL{h] P`P ;ߨܓTT)oO`Sg$].a8`C-j y~ZOZx2fx>yEݒP9T]WX V( RThЁ~ޔZV!U_B0"P.jUE s\ݮr*&5$0x\y6 Ž“m Kaj`1 t@ UČ^8/FMbAsmWdXqZ[b@ֹha`-ŘYqW=P'ltrtĤDF2;Gw~ ʽ)}A5fB(B۷o?[TEzKϥ|*nT!~[U|^^x4<<ʤςٍ8 >[P0&2jXs}}qvrHUa*fw#7T&k77),Gd}শYՒ뒨QPԌΡ &,O&b1<9ߩotzL<˨vaqY]C5]yZҀu0)VQᬂ25]A֥6J*C&[k<̚GRGΣ ;X_ϠY߸q}U|4;q&VAuZ1 EY3 RA_,QՋHOXS-l\!][>^Z73P'8H% v+P*TV<_7CWn*pQ]Yu*hy4'@UEkU*Ҝ]oU?B16Jyb^A$½K'1\~OZPi0z겹[ P;di2ռYdU|kG)Kv{xb7EMV*RayTafoTW [I7BSz\ -_bHAfhcTVN`#>\ycA$umxSlJf&^)/ ^CgKu9W$ فôưoP0FfB"RC'Ts7gb M/l͸@i9@`X(EƵwPy>y >:.-v`3)zR 0]c5 3 cnA"d=ڀ%%;PTԑ.]\lr )[T !cxR^R\1|Ѱ2#@tBʬ)k&a5+_t*V *6V`3=S+G fYQ i5 M{h$1/GU<6N8+"f@n$^c3ԑUB~%mڬDefmɸN @5z .Fr _O|U՞mP)6 }S޳X95Oiq $- Dʴ%}=6P\QҌMΊhG76غjvH$> ìA Վ<5RG50.8p֤ ՄvQ;3vP-]-T5T|v >aPv]dAG hpx6k3h'1ϱBjģfdPTH7xtrg# **!(b%RO+g1vˑXŖ|*à <5P` &vtw=6q=di+iV½[ ҦT>: ?!S"K^ 0>y LRϽ]k th Xgǥ㵅aL++H!~Yw(lt$cD ˑL1C]Ί tsJJ gAE_44*݊FS6 ")X5ڝF렠ch"Tw *YPeE9Jb1 "Kb2Iz+LEdB(T6O+_qUZnڨ a, /eT"d E_r`mٴb!T8ԪtX_'[.ow}7.ߨK?J^f=Bu[(%vA<9Qy>]Sjy_0~r|/bpxXLA#Xjj;ys`8ubskbJmP}=)vHVrQAoU h*2^Bou7Pzj1;[ W" Nj(ըkD%sgK,7m8 ׫`iG`~sRͮ"LR/T)+2+Lև 6λWe Կ!q}5%']9Iq < /"d3A3Ya1w")(V-fPfM' e{ä EX4pjBr=mnAA*fuLǴmޯۥrTBjb1o0Tݱ%J8n jkr-~${ow-7* 4JFI73bKOZTQ`Y< --Õ@h# M0rTekþ W_{dP RT]0Dѱ%*F\R{?MtRȺbIepۮ Y#) ( Q=Dæ2*,R 7]g. dtrvJ;T/19pȒ]P2BDZIJY K^ P5p{ z!mP !}B T ؊T(ֵM=8=lXFDQEbg侚ϩa2BaVuXB= M6iG'gt [A4[B'*t#D5:73=1 গl5Bey_\vkj&43eeUS&fjу~#uqFS/(dѳ`̳dgU0 ?fi*khcܶ( s7Hi "IDAT#1>pQj5zOna]aPHN*jd(Gi/1 k@-o[sO*S)=sm7Qp+춭=e{fCk:4㽻yafû dV/]bmC+ҍ*^ y:gsD|t5Y]ٚ2NI*\oz]rvK&USm{KO$<إh?YTf%yU4nbeTvTׯ޴oľAۭ~v_P1",ClPM-#%_`y=]MAJz'+[ `,gR!zBhHjU% ޠ3~9Z_R1il37/V,Q0u ۑڦAz7LW.R^ '@ ^qo7:58~-Iw fVHW?Y[LyPdmGk+_{y%C-JU7%DSK2%R*!Sz9VmO܇ufa/mjEO-#M%j["z/F|-^++/:¡ ˁ;^6!MlA21[b&N`.h.5iW#]e 7*첨!Wp{޿g>qlV5(4jbp:h[,VcT7ZL}$wCEH39A%ROs.]QZ,#ME"Z_ aZ%iW)vbѽ0 Mr5@Y+zDTv\+Pɻ͖2Zŀ//bjuM)۫UR z,A{%vr!kn^p' \|.h$3N8;7/\Zw*^0v)~ۖ^0`>1`%w 'ylI6Iur aogлkD&0 l -9#9HR8LkM)J!Y1Zq㒬}Oz[ P_tgK&zI-. [LZD/JcLa_GAsqGNz]g,/pDEEɅ#dkGsSJĕĕ~*mrhF"Ǔ30S/g= G 4^iW'-<>˕tfC.[wJڍ1gai0\ !I~.oy}Kmy:8W_|>ڢ͜58lVReml>gD 20 Pi oŒr{먢 OƗ6(s׋ 2:J {R&8GgV#{*C_k܏JR[ ݄;@ 3o$z1q7 *2 *N{,*U'<r kmYvḾu!0'|߱uG@Jeyq/[ny󭋑APP\tQ?\xbDb_Rg85u+͉ e.@ f.[\f8"AE6 fs<,Y̖d .Mx34(kf5v2bpL:T d9k⹙k^5OȰ*DS.P = ]>A"PGEXffT@{~qY>\ݴ/җf79daH`O Ôb5Tl:B/P]J굗ٚJ*2Wt)Q@~* ,`>b3URvGYu 8T6n.OZxy G-޻P*? h2L:*֚X&b PB59j/jv꽖 k̒+נ*00EdTKEQvIؓ>l*"s*7m7ńZbV$lfj&6 P5._JJ):/98JTIGZn (hBʑu{ٌ+,gOU/WZſ 5ͣ}GϜ|ỵ]Q4wBвr՗5" T^0_is؂;Wȧ L"Twgʌh $ b鄍k(nIqirE/#sm1&oNw,u ]  (T ~r5If?Լ<#W5+5Yib\;TCk7BZ庂 {:rEʌ]"@GcTҢ_*9k3jFS*,2k(Zo]q쩞rpE,\041'~N΋#Ss^ 137;8ϰSO¥RAA^\*0fY X)řWˌk!b }׿<%aT1 llvP\=9/$gCYn>eDpNB帨|=\UI:i~OҪtRO7$!~Lra Hv'lW"LTirF ,gU2ebm]|m:@9JkZѮ-lۥ&>"yPKXd)YcRhqgd/yWsϫȐ1TGVݚ2 BsyuSSa|OEaz2V—&]_9*W͈2{&R 褐}y+}DqE ]61xL/_'y}OW}]CbWJ|~ %8:{I=-< ]r<}QN;w"ev>QMOkz]Tk rNu(^axt#:ؗafNF،[+s8Ƀ =]B\9O/mae}i2Nq:u2Fom!k\OI˼)8eٸ!@\_9{o%,=*vUU^@ JǤ'ns;l3٬U6zZѕ˙EM*Z-ܻO[]_cDYK!U͋$zK9ٌz{y#/^i*E867*씷 *<Ϲ|؁YLumdC(zM/ Ĺ~x$K:s۷Lj5=BLD y#N@~AU,۩^y^sghgΞ}ȅ{̹w]}|뙳?|[>:sq^-d-P5|X7t,7uϼ[O c,=J]%_gOp?w88d9#EX>SkUUMgĕUߟ+|UT -F;:r))u8YyLjo>{8{?]M}^+7bS g)QJD~#|O/=я~#Oҟ:zcd- ]IENDB`babel-2.14.0/docs/support.rst0000644000175000017500000000327414536056757015417 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- ============================= Support Classes and Functions ============================= The ``babel.support`` modules contains a number of classes and functions that can help with integrating Babel, and internationalization in general, into your application or framework. The code in this module is not used by Babel itself, but instead is provided to address common requirements of applications that should handle internationalization. --------------- Lazy Evaluation --------------- One such requirement is lazy evaluation of translations. Many web-based applications define some localizable message at the module level, or in general at some level where the locale of the remote user is not yet known. For such cases, web frameworks generally provide a "lazy" variant of the ``gettext`` functions, which basically translates the message not when the ``gettext`` function is invoked, but when the string is accessed in some manner. --------------------------- Extended Translations Class --------------------------- Many web-based applications are composed of a variety of different components (possibly using some kind of plugin system), and some of those components may provide their own message catalogs that need to be integrated into the larger system. To support this usage pattern, Babel provides a ``Translations`` class that is derived from the ``GNUTranslations`` class in the ``gettext`` module. This class adds a ``merge()`` method that takes another ``Translations`` instance, and merges the content of the latter into the main catalog: .. code-block:: python translations = Translations.load('main') translations.merge(Translations.load('plugin1')) babel-2.14.0/docs/messages.rst0000644000175000017500000002677014536056757015520 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- .. _messages: ============================= Working with Message Catalogs ============================= Introduction ============ The ``gettext`` translation system enables you to mark any strings used in your application as subject to localization, by wrapping them in functions such as ``gettext(str)`` and ``ngettext(singular, plural, num)``. For brevity, the ``gettext`` function is often aliased to ``_(str)``, so you can write: .. code-block:: python print(_("Hello")) instead of just: .. code-block:: python print("Hello") to make the string "Hello" localizable. Message catalogs are collections of translations for such localizable messages used in an application. They are commonly stored in PO (Portable Object) and MO (Machine Object) files, the formats of which are defined by the GNU `gettext`_ tools and the GNU `translation project`_. .. _`gettext`: https://www.gnu.org/software/gettext/ .. _`translation project`: https://sourceforge.net/projects/translation/ The general procedure for building message catalogs looks something like this: * use a tool (such as ``xgettext``) to extract localizable strings from the code base and write them to a POT (PO Template) file. * make a copy of the POT file for a specific locale (for example, "en_US") and start translating the messages * use a tool such as ``msgfmt`` to compile the locale PO file into a binary MO file * later, when code changes make it necessary to update the translations, you regenerate the POT file and merge the changes into the various locale-specific PO files, for example using ``msgmerge`` Python provides the :mod:`gettext` module as part of the standard library, which enables applications to work with appropriately generated MO files. As ``gettext`` provides a solid and well supported foundation for translating application messages, Babel does not reinvent the wheel, but rather reuses this infrastructure, and makes it easier to build message catalogs for Python applications. Message Extraction ================== Babel provides functionality similar to that of the ``xgettext`` program, except that only extraction from Python source files is built-in, while support for other file formats can be added using a simple extension mechanism. Unlike ``xgettext``, which is usually invoked once for every file, the routines for message extraction in Babel operate on directories. While the per-file approach of ``xgettext`` works nicely with projects using a ``Makefile``, Python projects rarely use ``make``, and thus a different mechanism is needed for extracting messages from the heterogeneous collection of source files that many Python projects are composed of. When message extraction is based on directories instead of individual files, there needs to be a way to configure which files should be treated in which manner. For example, while many projects may contain ``.html`` files, some of those files may be static HTML files that don't contain localizable message, while others may be `Jinja2`_ templates, and still others may contain `Genshi`_ markup templates. Some projects may even mix HTML files for different templates languages (for whatever reason). Therefore the way in which messages are extracted from source files can not only depend on the file extension, but needs to be controllable in a precise manner. .. _`Jinja2`: http://jinja.pocoo.org/ .. _`Genshi`: https://genshi.edgewall.org/ Babel accepts a configuration file to specify this mapping of files to extraction methods, which is described below. .. _`frontends`: ---------- Front-Ends ---------- Babel provides two different front-ends to access its functionality for working with message catalogs: * A :ref:`cmdline`, and * :ref:`setup-integration` Which one you choose depends on the nature of your project. For most modern Python projects, the distutils/setuptools integration is probably more convenient. .. _`mapping`: ------------------------------------------- Extraction Method Mapping and Configuration ------------------------------------------- The mapping of extraction methods to files in Babel is done via a configuration file. This file maps extended glob patterns to the names of the extraction methods, and can also set various options for each pattern (which options are available depends on the specific extraction method). For example, the following configuration adds extraction of messages from both Genshi markup templates and text templates: .. code-block:: ini # Extraction from Python source files [python: **.py] # Extraction from Genshi HTML and text templates [genshi: **/templates/**.html] ignore_tags = script,style include_attrs = alt title summary [genshi: **/templates/**.txt] template_class = genshi.template:TextTemplate encoding = ISO-8819-15 # Extraction from JavaScript files [javascript: **.js] extract_messages = $._, jQuery._ The configuration file syntax is based on the format commonly found in ``.INI`` files on Windows systems, and as supported by the ``ConfigParser`` module in the Python standard library. Section names (the strings enclosed in square brackets) specify both the name of the extraction method, and the extended glob pattern to specify the files that this extraction method should be used for, separated by a colon. The options in the sections are passed to the extraction method. Which options are available is specific to the extraction method used. The extended glob patterns used in this configuration are similar to the glob patterns provided by most shells. A single asterisk (``*``) is a wildcard for any number of characters (except for the pathname component separator "/"), while a question mark (``?``) only matches a single character. In addition, two subsequent asterisk characters (``**``) can be used to make the wildcard match any directory level, so the pattern ``**.txt`` matches any file with the extension ``.txt`` in any directory. Lines that start with a ``#`` or ``;`` character are ignored and can be used for comments. Empty lines are ignored, too. .. note:: if you're performing message extraction using the command Babel provides for integration into ``setup.py`` scripts, you can also provide this configuration in a different way, namely as a keyword argument to the ``setup()`` function. See :ref:`setup-integration` for more information. Default Extraction Methods -------------------------- Babel comes with a few builtin extractors: ``python`` (which extracts messages from Python source files), ``javascript``, and ``ignore`` (which extracts nothing). The ``python`` extractor is by default mapped to the glob pattern ``**.py``, meaning it'll be applied to all files with the ``.py`` extension in any directory. If you specify your own mapping configuration, this default mapping is discarded, so you need to explicitly add it to your mapping (as shown in the example above.) .. _`referencing extraction methods`: Referencing Extraction Methods ------------------------------ To be able to use short extraction method names such as “genshi”, you need to have `pkg_resources`_ installed, and the package implementing that extraction method needs to have been installed with its meta data (the `egg-info`_). If this is not possible for some reason, you need to map the short names to fully qualified function names in an extract section in the mapping configuration. For example: .. code-block:: ini # Some custom extraction method [extractors] custom = mypackage.module:extract_custom [custom: **.ctm] some_option = foo Note that the builtin extraction methods ``python`` and ``ignore`` are available by default, even if `pkg_resources`_ is not installed. You should never need to explicitly define them in the ``[extractors]`` section. .. _`egg-info`: http://peak.telecommunity.com/DevCenter/PythonEggs .. _`pkg_resources`: http://peak.telecommunity.com/DevCenter/PkgResources -------------------------- Writing Extraction Methods -------------------------- Adding new methods for extracting localizable methods is easy. First, you'll need to implement a function that complies with the following interface: .. code-block:: python def extract_xxx(fileobj, keywords, comment_tags, options): """Extract messages from XXX files. :param fileobj: the file-like object the messages should be extracted from :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions :param comment_tags: a list of translator tags to search for and include in the results :param options: a dictionary of additional options (optional) :return: an iterator over ``(lineno, funcname, message, comments)`` tuples :rtype: ``iterator`` """ .. note:: Any strings in the tuples produced by this function must be either ``unicode`` objects, or ``str`` objects using plain ASCII characters. That means that if sources contain strings using other encodings, it is the job of the extractor implementation to do the decoding to ``unicode`` objects. Next, you should register that function as an entry point. This requires your ``setup.py`` script to use `setuptools`_, and your package to be installed with the necessary metadata. If that's taken care of, add something like the following to your ``setup.py`` script: .. code-block:: python def setup(... entry_points = """ [babel.extractors] xxx = your.package:extract_xxx """, That is, add your extraction method to the entry point group ``babel.extractors``, where the name of the entry point is the name that people will use to reference the extraction method, and the value being the module and the name of the function (separated by a colon) implementing the actual extraction. .. note:: As shown in `Referencing Extraction Methods`_, declaring an entry point is not strictly required, as users can still reference the extraction function directly. But whenever possible, the entry point should be declared to make configuration more convenient. .. _`setuptools`: http://peak.telecommunity.com/DevCenter/setuptools ------------------- Translator Comments ------------------- First of all what are comments tags. Comments tags are excerpts of text to search for in comments, only comments, right before the python :mod:`gettext` calls, as shown on the following example: .. code-block:: python # NOTE: This is a comment about `Foo Bar` _('Foo Bar') The comments tag for the above example would be ``NOTE:``, and the translator comment for that tag would be ``This is a comment about `Foo Bar```. The resulting output in the catalog template would be something like:: #. This is a comment about `Foo Bar` #: main.py:2 msgid "Foo Bar" msgstr "" Now, you might ask, why would I need that? Consider this simple case; you have a menu item called “manual”. You know what it means, but when the translator sees this they will wonder did you mean: 1. a document or help manual, or 2. a manual process? This is the simplest case where a translation comment such as “The installation manual” helps to clarify the situation and makes a translator more productive. .. note:: Whether translator comments can be extracted depends on the extraction method in use. The Python extractor provided by Babel does implement this feature, but others may not. babel-2.14.0/docs/dev.rst0000644000175000017500000000424514536056757014460 0ustar nileshnileshBabel Development ================= Babel as a library has a long history that goes back to the Trac project. Since then it has evolved into an independently developed project that implements data access for the CLDR project. This document tries to explain as best as possible the general rules of the project in case you want to help out developing. Tracking the CLDR ----------------- Generally the goal of the project is to work as closely as possible with the CLDR data. This has in the past caused some frustrating problems because the data is entirely out of our hand. To minimize the frustration we generally deal with CLDR updates the following way: * bump the CLDR data only with a major release of Babel. * never perform custom bugfixes on the CLDR data. * never work around CLDR bugs within Babel. If you find a problem in the data, report it upstream. * adjust the parsing of the data as soon as possible, otherwise this will spiral out of control later. This is especially the case for bigger updates that change pluralization and more. * try not to test against specific CLDR data that is likely to change. Python Versions --------------- At the moment the following Python versions should be supported: * Python 3.7 and up * PyPy 3.7 and up Unicode ------- Unicode is a big deal in Babel. Here is how the rules are set up: * internally everything is unicode that makes sense to have as unicode. * Encode / decode at boundaries explicitly. Never assume an encoding in a way it cannot be overridden. utf-8 should be generally considered the default encoding. Dates and Timezones ------------------- Babel's timezone support relies on either ``pytz`` or ``zoneinfo``; if ``pytz`` is installed, it is preferred over ``zoneinfo``. Babel should assume that any timezone objects can be from either of these modules. Assumptions to make: * use UTC where possible. * be super careful with local time. Do not use local time without knowing the exact timezone. * `time` without date is a very useless construct. Do not try to support timezones for it. If you do, assume that the current local date is assumed and not utc date. babel-2.14.0/docs/license.rst0000644000175000017500000000167714536056757015332 0ustar nileshnileshLicense ======= Babel is licensed under a three clause BSD License. It basically means: do whatever you want with it as long as the copyright in Babel sticks around, the conditions are not modified and the disclaimer is present. Furthermore you must not use the names of the authors to promote derivatives of the software without written consent. The full license text can be found below (:ref:`babel-license`). .. _authors: Authors ------- .. include:: ../AUTHORS General License Definitions --------------------------- The following section contains the full license texts for Babel and the documentation. - "AUTHORS" hereby refers to all the authors listed in the :ref:`authors` section. - The ":ref:`babel-license`" applies to all the sourcecode shipped as part of Babel (Babel itself as well as the examples and the unit tests) as well as documentation. .. _babel-license: Babel License ------------- .. include:: ../LICENSE babel-2.14.0/docs/setup.rst0000644000175000017500000003544314536056757015046 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- .. _setup-integration: ================================ Distutils/Setuptools Integration ================================ Babel provides commands for integration into ``setup.py`` scripts, based on either the ``distutils`` package that is part of the Python standard library, or the third-party ``setuptools`` package. These commands are available by default when Babel has been properly installed, and ``setup.py`` is using ``setuptools``. For projects that use plain old ``distutils``, the commands need to be registered explicitly, for example: .. code-block:: python from distutils.core import setup from babel.messages import frontend as babel setup( ... cmdclass = {'compile_catalog': babel.compile_catalog, 'extract_messages': babel.extract_messages, 'init_catalog': babel.init_catalog, 'update_catalog': babel.update_catalog} ) compile_catalog =============== The ``compile_catalog`` command is similar to the GNU ``msgfmt`` tool, in that it takes a message catalog from a PO file and compiles it to a binary MO file. If the command has been correctly installed or registered, a project's ``setup.py`` script should allow you to use the command:: $ ./setup.py compile_catalog --help Global options: --verbose (-v) run verbosely (default) --quiet (-q) run quietly (turns verbosity off) --dry-run (-n) don't actually do anything --help (-h) show detailed help message Options for 'compile_catalog' command: ... Running the command will produce a binary MO file:: $ ./setup.py compile_catalog --directory foobar/locale --locale pt_BR running compile_catalog compiling catalog to foobar/locale/pt_BR/LC_MESSAGES/messages.mo Options ------- The ``compile_catalog`` command accepts the following options: +-----------------------------+---------------------------------------------+ | Option | Description | +=============================+=============================================+ | ``--domain`` | domain of the PO file (defaults to | | | lower-cased project name) | +-----------------------------+---------------------------------------------+ | ``--directory`` (``-d``) | name of the base directory | +-----------------------------+---------------------------------------------+ | ``--input-file`` (``-i``) | name of the input file | +-----------------------------+---------------------------------------------+ | ``--output-file`` (``-o``) | name of the output file | +-----------------------------+---------------------------------------------+ | ``--locale`` (``-l``) | locale for the new localized string | +-----------------------------+---------------------------------------------+ | ``--use-fuzzy`` (``-f``) | also include "fuzzy" translations | +-----------------------------+---------------------------------------------+ | ``--statistics`` | print statistics about translations | +-----------------------------+---------------------------------------------+ If ``directory`` is specified, but ``output-file`` is not, the default filename of the output file will be:: //LC_MESSAGES/.mo If neither the ``input_file`` nor the ``locale`` option is set, this command looks for all catalog files in the base directory that match the given domain, and compiles each of them to MO files in the same directory. These options can either be specified on the command-line, or in the ``setup.cfg`` file. extract_messages ================ The ``extract_messages`` command is comparable to the GNU ``xgettext`` program: it can extract localizable messages from a variety of difference source files, and generate a PO (portable object) template file from the collected messages. If the command has been correctly installed or registered, a project's ``setup.py`` script should allow you to use the command:: $ ./setup.py extract_messages --help Global options: --verbose (-v) run verbosely (default) --quiet (-q) run quietly (turns verbosity off) --dry-run (-n) don't actually do anything --help (-h) show detailed help message Options for 'extract_messages' command: ... Running the command will produce a PO template file:: $ ./setup.py extract_messages --output-file foobar/locale/messages.pot running extract_messages extracting messages from foobar/__init__.py extracting messages from foobar/core.py ... writing PO template file to foobar/locale/messages.pot Method Mapping -------------- The mapping of file patterns to extraction methods (and options) can be specified using a configuration file that is pointed to using the ``--mapping-file`` option shown above. Alternatively, you can configure the mapping directly in ``setup.py`` using a keyword argument to the ``setup()`` function: .. code-block:: python setup(... message_extractors = { 'foobar': [ ('**.py', 'python', None), ('**/templates/**.html', 'genshi', None), ('**/templates/**.txt', 'genshi', { 'template_class': 'genshi.template:TextTemplate' }) ], }, ... ) Options ------- The ``extract_messages`` command accepts the following options: +-----------------------------+----------------------------------------------+ | Option | Description | +=============================+==============================================+ | ``--charset`` | charset to use in the output file | +-----------------------------+----------------------------------------------+ | ``--keywords`` (``-k``) | space-separated list of keywords to look for | | | in addition to the defaults | +-----------------------------+----------------------------------------------+ | ``--no-default-keywords`` | do not include the default keywords | +-----------------------------+----------------------------------------------+ | ``--mapping-file`` (``-F``) | path to the mapping configuration file | +-----------------------------+----------------------------------------------+ | ``--no-location`` | do not include location comments with | | | filename and line number | +-----------------------------+----------------------------------------------+ | ``--omit-header`` | do not include msgid "" entry in header | +-----------------------------+----------------------------------------------+ | ``--output-file`` (``-o``) | name of the output file | +-----------------------------+----------------------------------------------+ | ``--width`` (``-w``) | set output line width (default 76) | +-----------------------------+----------------------------------------------+ | ``--no-wrap`` | do not break long message lines, longer than | | | the output line width, into several lines | +-----------------------------+----------------------------------------------+ | ``--input-dirs`` | directories that should be scanned for | | | messages | +-----------------------------+----------------------------------------------+ | ``--sort-output`` | generate sorted output (default False) | +-----------------------------+----------------------------------------------+ | ``--sort-by-file`` | sort output by file location (default False) | +-----------------------------+----------------------------------------------+ | ``--msgid-bugs-address`` | set email address for message bug reports | +-----------------------------+----------------------------------------------+ | ``--copyright-holder`` | set copyright holder in output | +-----------------------------+----------------------------------------------+ | ``--add-comments (-c)`` | place comment block with TAG (or those | | | preceding keyword lines) in output file. | | | Separate multiple TAGs with commas(,) | +-----------------------------+----------------------------------------------+ These options can either be specified on the command-line, or in the ``setup.cfg`` file. In the latter case, the options above become entries of the section ``[extract_messages]``, and the option names are changed to use underscore characters instead of dashes, for example: .. code-block:: ini [extract_messages] keywords = _ gettext ngettext mapping_file = mapping.cfg width = 80 This would be equivalent to invoking the command from the command-line as follows:: $ setup.py extract_messages -k _ -k gettext -k ngettext -F mapping.cfg -w 80 Any path names are interpreted relative to the location of the ``setup.py`` file. For boolean options, use "true" or "false" values. init_catalog ============ The ``init_catalog`` command is basically equivalent to the GNU ``msginit`` program: it creates a new translation catalog based on a PO template file (POT). If the command has been correctly installed or registered, a project's ``setup.py`` script should allow you to use the command:: $ ./setup.py init_catalog --help Global options: --verbose (-v) run verbosely (default) --quiet (-q) run quietly (turns verbosity off) --dry-run (-n) don't actually do anything --help (-h) show detailed help message Options for 'init_catalog' command: ... Running the command will produce a PO file:: $ ./setup.py init_catalog -l fr -i foobar/locales/messages.pot \ -o foobar/locales/fr/messages.po running init_catalog creating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot' Options ------- The ``init_catalog`` command accepts the following options: +-----------------------------+---------------------------------------------+ | Option | Description | +=============================+=============================================+ | ``--domain`` | domain of the PO file (defaults to | | | lower-cased project name) | +-----------------------------+---------------------------------------------+ | ``--input-file`` (``-i``) | name of the input file | +-----------------------------+---------------------------------------------+ | ``--output-dir`` (``-d``) | name of the output directory | +-----------------------------+---------------------------------------------+ | ``--output-file`` (``-o``) | name of the output file | +-----------------------------+---------------------------------------------+ | ``--locale`` | locale for the new localized string | +-----------------------------+---------------------------------------------+ If ``output-dir`` is specified, but ``output-file`` is not, the default filename of the output file will be:: //LC_MESSAGES/.po These options can either be specified on the command-line, or in the ``setup.cfg`` file. update_catalog ============== The ``update_catalog`` command is basically equivalent to the GNU ``msgmerge`` program: it updates an existing translations catalog based on a PO template file (POT). If the command has been correctly installed or registered, a project's ``setup.py`` script should allow you to use the command:: $ ./setup.py update_catalog --help Global options: --verbose (-v) run verbosely (default) --quiet (-q) run quietly (turns verbosity off) --dry-run (-n) don't actually do anything --help (-h) show detailed help message Options for 'update_catalog' command: ... Running the command will update a PO file:: $ ./setup.py update_catalog -l fr -i foobar/locales/messages.pot \ -o foobar/locales/fr/messages.po running update_catalog updating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot' Options ------- The ``update_catalog`` command accepts the following options: +-------------------------------------+-------------------------------------+ | Option | Description | +=====================================+=====================================+ | ``--domain`` | domain of the PO file (defaults to | | | lower-cased project name) | +-------------------------------------+-------------------------------------+ | ``--input-file`` (``-i``) | name of the input file | +-------------------------------------+-------------------------------------+ | ``--output-dir`` (``-d``) | name of the output directory | +-------------------------------------+-------------------------------------+ | ``--output-file`` (``-o``) | name of the output file | +-------------------------------------+-------------------------------------+ | ``--locale`` | locale for the new localized string | +-------------------------------------+-------------------------------------+ | ``--ignore-obsolete`` | do not include obsolete messages in | | | the output | +-------------------------------------+-------------------------------------+ | ``--no-fuzzy-matching`` (``-N``) | do not use fuzzy matching | +-------------------------------------+-------------------------------------+ | ``--previous`` | keep previous msgids of translated | | | messages | +-------------------------------------+-------------------------------------+ If ``output-dir`` is specified, but ``output-file`` is not, the default filename of the output file will be:: //LC_MESSAGES/.po If neither the ``input_file`` nor the ``locale`` option is set, this command looks for all catalog files in the base directory that match the given domain, and updates each of them. These options can either be specified on the command-line, or in the ``setup.cfg`` file. babel-2.14.0/docs/installation.rst0000644000175000017500000000607014536056757016401 0ustar nileshnilesh.. _installation: Installation ============ Babel is distributed as a standard Python package fully set up with all the dependencies it needs. On Python versions where the standard library `zoneinfo`_ module is not available, `pytz`_ needs to be installed for timezone support. If `pytz`_ is installed, it is preferred over the standard library `zoneinfo`_ module where possible. .. _pytz: https://pythonhosted.org/pytz/ .. _zoneinfo: https://docs.python.org/3/library/zoneinfo.html .. _virtualenv: virtualenv ---------- Virtualenv is probably what you want to use during development, and if you have shell access to your production machines, you'll probably want to use it there, too. Use ``pip`` to install it:: $ sudo pip install virtualenv If you're on Windows, run it in a command-prompt window with administrator privileges, and leave out ``sudo``. Once you have virtualenv installed, just fire up a shell and create your own environment. I usually create a project folder and a `venv` folder within:: $ mkdir myproject $ cd myproject $ virtualenv venv New python executable in venv/bin/python Installing distribute............done. Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, do the following:: $ . venv/bin/activate If you are a Windows user, the following command is for you:: $ venv\scripts\activate Either way, you should now be using your virtualenv (notice how the prompt of your shell has changed to show the active environment). Now you can just enter the following command to get Babel installed in your virtualenv:: $ pip install Babel A few seconds later and you are good to go. System-Wide Installation ------------------------ This is possible as well, though I do not recommend it. Just run `pip` with root privileges:: $ sudo pip install Babel (On Windows systems, run it in a command-prompt window with administrator privileges, and leave out `sudo`.) Living on the Edge ------------------ If you want to work with the latest version of Babel, you will need to use a git checkout. Get the git checkout in a new virtualenv and run in development mode:: $ git clone https://github.com/python-babel/babel Initialized empty Git repository in ~/dev/babel/.git/ $ cd babel $ virtualenv venv New python executable in venv/bin/python Installing distribute............done. $ . venv/bin/activate $ python setup.py import_cldr $ pip install --editable . ... Finished processing dependencies for Babel Make sure to not forget about the ``import_cldr`` step because otherwise you will be missing the locale data. The custom setup command will download the most appropriate CLDR release from the official website and convert it for Babel. This will pull also in the dependencies and activate the git head as the current version inside the virtualenv. Then all you have to do is run ``git pull origin`` to update to the latest version. If the CLDR data changes you will have to re-run ``python setup.py import_cldr``. babel-2.14.0/docs/Makefile0000644000175000017500000001267014536056757014611 0ustar nileshnilesh# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Babel.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Babel.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Babel" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Babel" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." babel-2.14.0/docs/dates.rst0000644000175000017500000004154114536056757015002 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- .. _date-and-time: ============= Date and Time ============= When working with date and time information in Python, you commonly use the classes ``date``, ``datetime`` and/or ``time`` from the `datetime` package. Babel provides functions for locale-specific formatting of those objects in its ``dates`` module: .. code-block:: pycon >>> from datetime import date, datetime, time >>> from babel.dates import format_date, format_datetime, format_time >>> d = date(2007, 4, 1) >>> format_date(d, locale='en') u'Apr 1, 2007' >>> format_date(d, locale='de_DE') u'01.04.2007' As this example demonstrates, Babel will automatically choose a date format that is appropriate for the requested locale. The ``format_*()`` functions also accept an optional ``format`` argument, which allows you to choose between one of four format variations: * ``short``, * ``medium`` (the default), * ``long``, and * ``full``. For example: .. code-block:: pycon >>> format_date(d, format='short', locale='en') u'4/1/07' >>> format_date(d, format='long', locale='en') u'April 1, 2007' >>> format_date(d, format='full', locale='en') u'Sunday, April 1, 2007' Core Time Concepts ================== Working with dates and time can be a complicated thing. Babel attempts to simplify working with them by making some decisions for you. Python's datetime module has different ways to deal with times and dates: naive and timezone-aware datetime objects. Babel generally recommends you to store all your time in naive datetime objects and treat them as UTC at all times. This simplifies dealing with time a lot because otherwise you can get into the hairy situation where you are dealing with datetime objects of different timezones. That is tricky because there are situations where time can be ambiguous. This is usually the case when dealing with dates around timezone transitions. The most common case of timezone transition is changes between daylight saving time and standard time. As such we recommend to always use UTC internally and only reformat to local time when returning dates to users. At that point the timezone the user has selected can usually be established and Babel can automatically rebase the time for you. To get the current time use the :meth:`~datetime.datetime.now` method of the :class:`~datetime.datetime` object, passing :attr:`~datetime.timezone.utc` to it as the timezone. For more information about timezones see :ref:`timezone-support`. Pattern Syntax ============== While Babel makes it simple to use the appropriate date/time format for a given locale, you can also force it to use custom patterns. Note that Babel uses different patterns for specifying number and date formats compared to the Python equivalents (such as ``time.strftime()``), which have mostly been inherited from C and POSIX. The patterns used in Babel are based on the `Locale Data Markup Language specification`_ (LDML), which defines them as follows: A date/time pattern is a string of characters, where specific strings of characters are replaced with date and time data from a calendar when formatting or used to generate data for a calendar when parsing. […] Characters may be used multiple times. For example, if ``y`` is used for the year, ``yy`` might produce "99", whereas ``yyyy`` produces "1999". For most numerical fields, the number of characters specifies the field width. For example, if ``h`` is the hour, ``h`` might produce "5", but ``hh`` produces "05". For some characters, the count specifies whether an abbreviated or full form should be used […] Two single quotes represent a literal single quote, either inside or outside single quotes. Text within single quotes is not interpreted in any way (except for two adjacent single quotes). For example: .. code-block:: pycon >>> d = date(2007, 4, 1) >>> format_date(d, "EEE, MMM d, ''yy", locale='en') u"Sun, Apr 1, '07" >>> format_date(d, "EEEE, d.M.yyyy", locale='de') u'Sonntag, 1.4.2007' >>> t = time(15, 30) >>> format_time(t, "hh 'o''clock' a", locale='en') u"03 o'clock PM" >>> format_time(t, 'H:mm a', locale='de') u'15:30 nachm.' >>> dt = datetime(2007, 4, 1, 15, 30) >>> format_datetime(dt, "yyyyy.MMMM.dd GGG hh:mm a", locale='en') u'02007.April.01 AD 03:30 PM' The syntax for custom datetime format patterns is described in detail in the the `Locale Data Markup Language specification`_. The following table is just a relatively brief overview. .. _`Locale Data Markup Language specification`: https://unicode.org/reports/tr35/#Date_Format_Patterns Date Fields ----------- +----------+--------+--------------------------------------------------------+ | Field | Symbol | Description | +==========+========+========================================================+ | Era | ``G`` | Replaced with the era string for the current date. One | | | | to three letters for the abbreviated form, four | | | | lettersfor the long form, five for the narrow form | +----------+--------+--------------------------------------------------------+ | Year | ``y`` | Replaced by the year. Normally the length specifies | | | | the padding, but for two letters it also specifies the | | | | maximum length. | | +--------+--------------------------------------------------------+ | | ``Y`` | Same as ``y`` but uses the ISO year-week calendar. ISO | | | | year-week increments after completing the last week of | | | | the year. Therefore it may change a few days before or | | | | after ``y``. Recommend use with the ``w`` Symbol. | | +--------+--------------------------------------------------------+ | | ``u`` | ?? | +----------+--------+--------------------------------------------------------+ | Quarter | ``Q`` | Use one or two for the numerical quarter, three for | | | | the abbreviation, or four for the full name. | | +--------+--------------------------------------------------------+ | | ``q`` | Use one or two for the numerical quarter, three for | | | | the abbreviation, or four for the full name. | +----------+--------+--------------------------------------------------------+ | Month | ``M`` | Use one or two for the numerical month, three for the | | | | abbreviation, or four for the full name, or five for | | | | the narrow name. | | +--------+--------------------------------------------------------+ | | ``L`` | Use one or two for the numerical month, three for the | | | | abbreviation, or four for the full name, or 5 for the | | | | narrow name. | +----------+--------+--------------------------------------------------------+ | Week | ``w`` | Week of year according to the ISO year-week calendar. | | | | This may have 52 or 53 weeks depending on the year. | | | | Recommend use with the ``Y`` symbol. | | +--------+--------------------------------------------------------+ | | ``W`` | Week of month. | +----------+--------+--------------------------------------------------------+ | Day | ``d`` | Day of month. | | +--------+--------------------------------------------------------+ | | ``D`` | Day of year. | | +--------+--------------------------------------------------------+ | | ``F`` | Day of week in month. | | +--------+--------------------------------------------------------+ | | ``g`` | ?? | +----------+--------+--------------------------------------------------------+ | Week day | ``E`` | Day of week. Use one through three letters for the | | | | short day, or four for the full name, or five for the | | | | narrow name. | | +--------+--------------------------------------------------------+ | | ``e`` | Local day of week. Same as E except adds a numeric | | | | value that will depend on the local starting day of | | | | the week, using one or two letters. | | +--------+--------------------------------------------------------+ | | ``c`` | ?? | +----------+--------+--------------------------------------------------------+ Time Fields ----------- +----------+--------+--------------------------------------------------------+ | Field | Symbol | Description | +==========+========+========================================================+ | Period | ``a`` | AM or PM | +----------+--------+--------------------------------------------------------+ | Hour | ``h`` | Hour [1-12]. | | +--------+--------------------------------------------------------+ | | ``H`` | Hour [0-23]. | | +--------+--------------------------------------------------------+ | | ``K`` | Hour [0-11]. | | +--------+--------------------------------------------------------+ | | ``k`` | Hour [1-24]. | +----------+--------+--------------------------------------------------------+ | Minute | ``m`` | Use one or two for zero places padding. | +----------+--------+--------------------------------------------------------+ | Second | ``s`` | Use one or two for zero places padding. | | +--------+--------------------------------------------------------+ | | ``S`` | Fractional second, rounds to the count of letters. | | +--------+--------------------------------------------------------+ | | ``A`` | Milliseconds in day. | +----------+--------+--------------------------------------------------------+ | Timezone | ``z`` | Use one to three letters for the short timezone or | | | | four for the full name. | | +--------+--------------------------------------------------------+ | | ``Z`` | Use one to three letters for RFC 822, four letters for | | | | GMT format. | | +--------+--------------------------------------------------------+ | | ``v`` | Use one letter for short wall (generic) time, four for | | | | long wall time. | | +--------+--------------------------------------------------------+ | | ``V`` | Same as ``z``, except that timezone abbreviations | | | | should be used regardless of whether they are in | | | | common use by the locale. | +----------+--------+--------------------------------------------------------+ Time Delta Formatting ===================== In addition to providing functions for formatting localized dates and times, the ``babel.dates`` module also provides a function to format the difference between two times, called a ''time delta''. These are usually represented as ``datetime.timedelta`` objects in Python, and it's also what you get when you subtract one ``datetime`` object from an other. The ``format_timedelta`` function takes a ``timedelta`` object and returns a human-readable representation. This happens at the cost of precision, as it chooses only the most significant unit (such as year, week, or hour) of the difference, and displays that: .. code-block:: pycon >>> from datetime import timedelta >>> from babel.dates import format_timedelta >>> delta = timedelta(days=6) >>> format_timedelta(delta, locale='en_US') u'1 week' The resulting strings are based from the CLDR data, and are properly pluralized depending on the plural rules of the locale and the calculated number of units. The function provides parameters for you to influence how this most significant unit is chosen: with ``threshold`` you set the value after which the presentation switches to the next larger unit, and with ``granularity`` you can limit the smallest unit to display: .. code-block:: pycon >>> delta = timedelta(days=6) >>> format_timedelta(delta, threshold=1.2, locale='en_US') u'6 days' >>> format_timedelta(delta, granularity='month', locale='en_US') u'1 month' .. _timezone-support: Time-zone Support ================= Many of the verbose time formats include the time-zone, but time-zone information is not by default available for the Python ``datetime`` and ``time`` objects. The standard library includes only the abstract ``tzinfo`` class, which you need appropriate implementations for to actually use in your application. Babel includes a ``tzinfo`` implementation for UTC (Universal Time). Babel uses either `zoneinfo`_ or `pytz`_ for timezone support. If pytz is installed, it is preferred over the standard library's zoneinfo. You can directly interface with either of these modules from within Babel: .. code-block:: pycon >>> from datetime import time >>> from babel.dates import get_timezone, UTC >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=UTC) >>> eastern = get_timezone('US/Eastern') >>> format_datetime(dt, 'H:mm Z', tzinfo=eastern, locale='en_US') u'11:30 -0400' The recommended approach to deal with different time-zones in a Python application is to always use UTC internally, and only convert from/to the users time-zone when accepting user input and displaying date/time data, respectively. You can use Babel together with ``zoneinfo`` or ``pytz`` to apply a time-zone to any ``datetime`` or ``time`` object for display, leaving the original information unchanged: .. code-block:: pycon >>> british = get_timezone('Europe/London') >>> format_datetime(dt, 'H:mm zzzz', tzinfo=british, locale='en_US') u'16:30 British Summer Time' Here, the given UTC time is adjusted to the "Europe/London" time-zone, and daylight savings time is taken into account. Daylight savings time is also applied to ``format_time``, but because the actual date is unknown in that case, the current day is assumed to determine whether DST or standard time should be used. Babel also provides support for working with the local timezone of your operating system. It's provided through the ``LOCALTZ`` constant: .. code-block:: pycon >>> from babel.dates import LOCALTZ, get_timezone_name >>> LOCALTZ >>> get_timezone_name(LOCALTZ) u'Central European Time' .. _pytz: https://pythonhosted.org/pytz/ Localized Time-zone Names ------------------------- While the ``Locale`` class provides access to various locale display names related to time-zones, the process of building a localized name of a time-zone is actually quite complicated. Babel implements it in separately usable functions in the ``babel.dates`` module, most importantly the ``get_timezone_name`` function: .. code-block:: pycon >>> from babel import Locale >>> from babel.dates import get_timezone_name, get_timezone >>> tz = get_timezone('Europe/Berlin') >>> get_timezone_name(tz, locale=Locale.parse('pt_PT')) u'Hora da Europa Central' You can pass the function either a ``datetime.tzinfo`` object, or a ``datetime.date`` or ``datetime.datetime`` object. If you pass an actual date, the function will be able to take daylight savings time into account. If you pass just the time-zone, Babel does not know whether daylight savings time is in effect, so it uses a generic representation, which is useful for example to display a list of time-zones to the user. .. code-block:: pycon >>> from datetime import datetime >>> from babel.dates import _localize >>> dt = _localize(tz, datetime(2007, 8, 15)) >>> get_timezone_name(dt, locale=Locale.parse('de_DE')) u'Mitteleurop\xe4ische Sommerzeit' >>> get_timezone_name(tz, locale=Locale.parse('de_DE')) u'Mitteleurop\xe4ische Zeit' babel-2.14.0/docs/numbers.rst0000644000175000017500000001666614536056757015367 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- .. _numbers: ================= Number Formatting ================= Support for locale-specific formatting and parsing of numbers is provided by the ``babel.numbers`` module: .. code-block:: pycon >>> from babel.numbers import format_number, format_decimal, format_compact_decimal, format_percent Examples: .. code-block:: pycon # Numbers with decimal places >>> format_decimal(1.2345, locale='en_US') u'1.234' >>> format_decimal(1.2345, locale='sv_SE') u'1,234' # Integers with thousand grouping >>> format_decimal(12345, locale='de_DE') u'12.345' >>> format_decimal(12345678, locale='de_DE') u'12.345.678' Pattern Syntax ============== While Babel makes it simple to use the appropriate number format for a given locale, you can also force it to use custom patterns. As with date/time formatting patterns, the patterns Babel supports for number formatting are based on the `Locale Data Markup Language specification`_ (LDML). Examples: .. code-block:: pycon >>> format_decimal(-1.2345, format='#,##0.##;-#', locale='en') u'-1.23' >>> format_decimal(-1.2345, format='#,##0.##;(#)', locale='en') u'(1.23)' The syntax for custom number format patterns is described in detail in the the specification. The following table is just a relatively brief overview. .. _`Locale Data Markup Language specification`: https://unicode.org/reports/tr35/#Number_Format_Patterns +----------+-----------------------------------------------------------------+ | Symbol | Description | +==========+=================================================================+ | ``0`` | Digit | +----------+-----------------------------------------------------------------+ | ``1-9`` | '1' through '9' indicate rounding. | +----------+-----------------------------------------------------------------+ | ``@`` | Significant digit | +----------+-----------------------------------------------------------------+ | ``#`` | Digit, zero shows as absent | +----------+-----------------------------------------------------------------+ | ``.`` | Decimal separator or monetary decimal separator | +----------+-----------------------------------------------------------------+ | ``-`` | Minus sign | +----------+-----------------------------------------------------------------+ | ``,`` | Grouping separator | +----------+-----------------------------------------------------------------+ | ``E`` | Separates mantissa and exponent in scientific notation | +----------+-----------------------------------------------------------------+ | ``+`` | Prefix positive exponents with localized plus sign | +----------+-----------------------------------------------------------------+ | ``;`` | Separates positive and negative subpatterns | +----------+-----------------------------------------------------------------+ | ``%`` | Multiply by 100 and show as percentage | +----------+-----------------------------------------------------------------+ | ``‰`` | Multiply by 1000 and show as per mille | +----------+-----------------------------------------------------------------+ | ``¤`` | Currency sign, replaced by currency symbol. If doubled, | | | replaced by international currency symbol. If tripled, uses the | | | long form of the decimal symbol. | +----------+-----------------------------------------------------------------+ | ``'`` | Used to quote special characters in a prefix or suffix | +----------+-----------------------------------------------------------------+ | ``*`` | Pad escape, precedes pad character | +----------+-----------------------------------------------------------------+ Rounding Modes ============== Since Babel makes full use of Python's `Decimal`_ type to perform number rounding before formatting, users have the chance to control the rounding mode and other configurable parameters through the active `Context`_ instance. By default, Python rounding mode is ``ROUND_HALF_EVEN`` which complies with `UTS #35 section 3.3`_. Yet, the caller has the opportunity to tweak the current context before formatting a number or currency: .. code-block:: pycon >>> from babel.numbers import decimal, format_decimal >>> with decimal.localcontext(decimal.Context(rounding=decimal.ROUND_DOWN)): >>> txt = format_decimal(123.99, format='#', locale='en_US') >>> txt u'123' It is also possible to use ``decimal.setcontext`` or directly modifying the instance returned by ``decimal.getcontext``. However, using a context manager is always more convenient due to the automatic restoration and the ability to nest them. Whatever mechanism is chosen, always make use of the ``decimal`` module imported from ``babel.numbers``. For efficiency reasons, Babel uses the fastest decimal implementation available, such as `cdecimal`_. These various implementation offer an identical API, but their types and instances do **not** interoperate with each other. For example, the previous example can be slightly modified to generate unexpected results on Python 2.7, with the `cdecimal`_ module installed: .. code-block:: pycon >>> from decimal import localcontext, Context, ROUND_DOWN >>> from babel.numbers import format_decimal >>> with localcontext(Context(rounding=ROUND_DOWN)): >>> txt = format_decimal(123.99, format='#', locale='en_US') >>> txt u'124' Changing other parameters such as the precision may also alter the results of the number formatting functions. Remember to test your code to make sure it behaves as desired. .. _Decimal: https://docs.python.org/3/library/decimal.html#decimal-objects .. _Context: https://docs.python.org/3/library/decimal.html#context-objects .. _`UTS #35 section 3.3`: https://www.unicode.org/reports/tr35/tr35-numbers.html#Formatting .. _cdecimal: https://pypi.org/project/cdecimal/ Parsing Numbers =============== Babel can also parse numeric data in a locale-sensitive manner: .. code-block:: pycon >>> from babel.numbers import parse_decimal, parse_number Examples: .. code-block:: pycon >>> parse_decimal('1,099.98', locale='en_US') 1099.98 >>> parse_decimal('1.099,98', locale='de') 1099.98 >>> parse_decimal('2,109,998', locale='de') Traceback (most recent call last): ... NumberFormatError: '2,109,998' is not a valid decimal number Note: as of version 2.8.0, the ``parse_number`` function has limited functionality. It can remove group symbols of certain locales from numeric strings, but may behave unexpectedly until its logic handles more encoding issues and other special cases. Examples: .. code-block:: pycon >>> parse_number('1,099', locale='en_US') 1099 >>> parse_number('1.099.024', locale='de') 1099024 >>> parse_number('123' + u'\xa0' + '4567', locale='ru') 1234567 >>> parse_number('123 4567', locale='ru') ... NumberFormatError: '123 4567' is not a valid number babel-2.14.0/docs/make.bat0000644000175000017500000001174614536056757014561 0ustar nileshnilesh@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Babel.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Babel.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end babel-2.14.0/docs/intro.rst0000644000175000017500000000417414536056757015036 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- ============ Introduction ============ The functionality Babel provides for internationalization (I18n) and localization (L10N) can be separated into two different aspects: * tools to build and work with ``gettext`` message catalogs, and * a Python interface to the CLDR (Common Locale Data Repository), providing access to various locale display names, localized number and date formatting, etc. Message Catalogs ================ While the Python standard library includes a :mod:`gettext` module that enables applications to use message catalogs, it requires developers to build these catalogs using GNU tools such as ``xgettext``, ``msgmerge``, and ``msgfmt``. And while ``xgettext`` does have support for extracting messages from Python files, it does not know how to deal with other kinds of files commonly found in Python web-applications, such as templates, nor does it provide an easy extensibility mechanism to add such support. Babel addresses this by providing a framework where various extraction methods can be plugged in to a larger message extraction framework, and also removes the dependency on the GNU ``gettext`` tools for common tasks, as these aren't necessarily available on all platforms. See :ref:`messages` for details on this aspect of Babel. Locale Data =========== Furthermore, while the Python standard library does include support for basic localization with respect to the formatting of numbers and dates (the :mod:`locale` module, among others), this support is based on the assumption that there will be only one specific locale used per process (at least simultaneously.) Also, it doesn't provide access to other kinds of locale data, such as the localized names of countries, languages, or time-zones, which are frequently needed in web-based applications. For these requirements, Babel includes data extracted from the `Common Locale Data Repository (CLDR) `_, and provides a number of convenient methods for accessing and using this data. See :ref:`locale-data`, :ref:`date-and-time`, and :ref:`numbers` for more information on this aspect of Babel. babel-2.14.0/docs/locale.rst0000644000175000017500000001043114536056757015133 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- .. _locale-data: =========== Locale Data =========== While :ref:`message catalogs ` allow you to localize any messages in your application, there are a number of strings that are used in many applications for which translations are readily available. Imagine for example you have a list of countries that users can choose from, and you'd like to display the names of those countries in the language the user prefers. Instead of translating all those country names yourself in your application, you can make use of the translations provided by the locale data included with Babel, which is based on the `Common Locale Data Repository (CLDR) `_ developed and maintained by the `Unicode Consortium `_. The ``Locale`` Class ==================== You normally access such locale data through the :class:`~babel.core.Locale` class provided by Babel: .. code-block:: pycon >>> from babel import Locale >>> locale = Locale('en', 'US') >>> locale.territories['US'] u'United States' >>> locale = Locale('es', 'MX') >>> locale.territories['US'] u'Estados Unidos' In addition to country/territory names, the locale data also provides access to names of languages, scripts, variants, time zones, and more. Some of the data is closely related to number and date formatting. Most of the corresponding ``Locale`` properties return dictionaries, where the key is a code such as the ISO country and language codes. Consult the API documentation for references to the relevant specifications. Likely Subtags ============== When dealing with locales you can run into the situation where a locale tag is not fully descriptive. For instance people commonly refer to ``zh_TW`` but that identifier does not resolve to a locale that the CLDR covers. Babel's locale identifier parser in that case will attempt to resolve the most likely subtag to end up with the intended locale: .. code-block:: pycon >>> from babel import Locale >>> Locale.parse('zh_TW') Locale('zh', territory='TW', script='Hant') This can also be used to find the most appropriate locale for a territory. In that case the territory code needs to be prefixed with ``und`` (unknown language identifier): .. code-block:: pycon >>> Locale.parse('und_AZ') Locale('az', territory='AZ', script='Latn') >>> Locale.parse('und_DE') Locale('de', territory='DE') Babel currently cannot deal with fuzzy locales (a locale not fully backed by data files) so we only accept locales that are fully backed by CLDR data. This will change in the future, but for the time being this restriction is in place. Locale Display Names ==================== Locales itself can be used to describe the locale itself or other locales. This mainly means that given a locale object you can ask it for its canonical display name, the name of the language and other things. Since the locales cross-reference each other you can ask for locale names in any language supported by the CLDR: .. code-block:: pycon >>> l = Locale.parse('de_DE') >>> l.get_display_name('en_US') u'German (Germany)' >>> l.get_display_name('fr_FR') u'allemand (Allemagne)' Display names include all the information to uniquely identify a locale (language, territory, script and variant) which is often not what you want. You can also ask for the information in parts: .. code-block:: pycon >>> l.get_language_name('de_DE') u'Deutsch' >>> l.get_language_name('it_IT') u'tedesco' >>> l.get_territory_name('it_IT') u'Germania' >>> l.get_territory_name('pt_PT') u'Alemanha' Calendar Display Names ====================== The :class:`~babel.core.Locale` class provides access to many locale display names related to calendar display, such as the names of weekdays or months. These display names are of course used for date formatting, but can also be used, for example, to show a list of months to the user in their preferred language: .. code-block:: pycon >>> locale = Locale('es') >>> month_names = locale.months['format']['wide'].items() >>> for idx, name in sorted(month_names): ... print name enero febrero marzo abril mayo junio julio agosto septiembre octubre noviembre diciembre babel-2.14.0/docs/api/0000755000175000017500000000000014536056757013714 5ustar nileshnileshbabel-2.14.0/docs/api/languages.rst0000644000175000017500000000044214536056757016414 0ustar nileshnileshLanguages ========= .. module:: babel.languages The languages module provides functionality to access data about languages that is not bound to a given locale. Official Languages ------------------ .. autofunction:: get_official_languages .. autofunction:: get_territory_language_info babel-2.14.0/docs/api/lists.rst0000644000175000017500000000024214536056757015602 0ustar nileshnileshList Formatting =============== .. module:: babel.lists This module lets you format lists of items in a locale-dependent manner. .. autofunction:: format_list babel-2.14.0/docs/api/plural.rst0000644000175000017500000000073714536056757015754 0ustar nileshnileshPluralization Support ===================== .. module:: babel.plural The pluralization support provides functionality around the CLDR pluralization rules. It can parse and evaluate pluralization rules, as well as convert them to other formats such as gettext. Basic Interface --------------- .. autoclass:: PluralRule :members: Conversion Functionality ------------------------ .. autofunction:: to_javascript .. autofunction:: to_python .. autofunction:: to_gettext babel-2.14.0/docs/api/core.rst0000644000175000017500000000131214536056757015373 0ustar nileshnileshCore Functionality ================== .. module:: babel.core The core API provides the basic core functionality. Primarily it provides the :class:`Locale` object and ways to create it. This object encapsulates a locale and exposes all the data it contains. All the core functionality is also directly importable from the `babel` module for convenience. Basic Interface --------------- .. autoclass:: Locale :members: .. autofunction:: default_locale .. autofunction:: negotiate_locale Exceptions ---------- .. autoexception:: UnknownLocaleError :members: Utility Functions ----------------- .. autofunction:: get_global .. autofunction:: parse_locale .. autofunction:: get_locale_identifier babel-2.14.0/docs/api/units.rst0000644000175000017500000000035014536056757015606 0ustar nileshnileshUnits ===== .. module:: babel.units The unit module provides functionality to format measurement units for different locales. .. autofunction:: format_unit .. autofunction:: format_compound_unit .. autofunction:: get_unit_name babel-2.14.0/docs/api/support.rst0000644000175000017500000000064214536056757016164 0ustar nileshnileshGeneral Support Functionality ============================= .. module:: babel.support Babel ships a few general helpers that are not being used by Babel itself but are useful in combination with functionality provided by it. Convenience Helpers ------------------- .. autoclass:: Format :members: .. autoclass:: LazyProxy :members: Gettext Support --------------- .. autoclass:: Translations :members: babel-2.14.0/docs/api/messages/0000755000175000017500000000000014536056757015523 5ustar nileshnileshbabel-2.14.0/docs/api/messages/catalog.rst0000644000175000017500000000072714536056757017675 0ustar nileshnileshMessages and Catalogs ===================== .. module:: babel.messages.catalog This module provides a basic interface to hold catalog and message information. It's generally used to modify a gettext catalog but it is not being used to actually use the translations. Catalogs -------- .. autoclass:: Catalog :members: :special-members: __iter__ Messages -------- .. autoclass:: Message :members: Exceptions ---------- .. autoexception:: TranslationError babel-2.14.0/docs/api/messages/mofile.rst0000644000175000017500000000041714536056757017532 0ustar nileshnileshMO File Support =============== .. module:: babel.messages.mofile The MO file support can read and write MO files. It reads them into :class:`~babel.messages.catalog.Catalog` objects and also writes catalogs out. .. autofunction:: read_mo .. autofunction:: write_mo babel-2.14.0/docs/api/messages/extract.rst0000644000175000017500000000172014536056757017727 0ustar nileshnileshLow-Level Extraction Interface ============================== .. module:: babel.messages.extract The low level extraction interface can be used to extract from directories or files directly. Normally this is not needed as the command line tools can do that for you. Extraction Functions -------------------- The extraction functions are what the command line tools use internally to extract strings. .. autofunction:: extract_from_dir .. autofunction:: extract_from_file .. autofunction:: extract Language Parsing ---------------- The language parsing functions are used to extract strings out of source files. These are automatically being used by the extraction functions but sometimes it can be useful to register wrapper functions, then these low level functions can be invoked. New functions can be registered through the setuptools entrypoint system. .. autofunction:: extract_python .. autofunction:: extract_javascript .. autofunction:: extract_nothing babel-2.14.0/docs/api/messages/pofile.rst0000644000175000017500000000042714536056757017536 0ustar nileshnileshPO File Support =============== .. module:: babel.messages.pofile The PO file support can read and write PO and POT files. It reads them into :class:`~babel.messages.catalog.Catalog` objects and also writes catalogs out. .. autofunction:: read_po .. autofunction:: write_po babel-2.14.0/docs/api/messages/index.rst0000644000175000017500000000035114536056757017363 0ustar nileshnileshMessages and Catalogs ===================== Babel provides functionality to work with message catalogs. This part of the API documentation shows those parts. .. toctree:: :maxdepth: 2 catalog extract mofile pofile babel-2.14.0/docs/api/dates.rst0000644000175000017500000000330714536056757015551 0ustar nileshnileshDate and Time ============= .. module:: babel.dates The date and time functionality provided by Babel lets you format standard Python `datetime`, `date` and `time` objects and work with timezones. Date and Time Formatting ------------------------ .. autofunction:: format_datetime(datetime=None, format='medium', tzinfo=None, locale=default_locale('LC_TIME')) .. autofunction:: format_date(date=None, format='medium', locale=default_locale('LC_TIME')) .. autofunction:: format_time(time=None, format='medium', tzinfo=None, locale=default_locale('LC_TIME')) .. autofunction:: format_timedelta(delta, granularity='second', threshold=.85, add_direction=False, format='long', locale=default_locale('LC_TIME')) .. autofunction:: format_skeleton(skeleton, datetime=None, tzinfo=None, fuzzy=True, locale=default_locale('LC_TIME')) .. autofunction:: format_interval(start, end, skeleton=None, tzinfo=None, fuzzy=True, locale=default_locale('LC_TIME')) Timezone Functionality ---------------------- .. autofunction:: get_timezone .. autofunction:: get_timezone_gmt .. autofunction:: get_timezone_location .. autofunction:: get_timezone_name .. data:: UTC A timezone object for UTC. .. data:: LOCALTZ A timezone object for the computer's local timezone. .. autoclass:: TimezoneTransition Data Access ----------- .. autofunction:: get_period_names .. autofunction:: get_day_names .. autofunction:: get_month_names .. autofunction:: get_quarter_names .. autofunction:: get_era_names .. autofunction:: get_date_format .. autofunction:: get_datetime_format .. autofunction:: get_time_format Basic Parsing ------------- .. autofunction:: parse_date .. autofunction:: parse_time .. autofunction:: parse_pattern babel-2.14.0/docs/api/numbers.rst0000644000175000017500000000173114536056757016123 0ustar nileshnileshNumbers and Currencies ====================== .. module:: babel.numbers The number module provides functionality to format numbers for different locales. This includes arbitrary numbers as well as currency. Number Formatting ----------------- .. autofunction:: format_number .. autofunction:: format_decimal .. autofunction:: format_compact_decimal .. autofunction:: format_currency .. autofunction:: format_compact_currency .. autofunction:: format_percent .. autofunction:: format_scientific Number Parsing -------------- .. autofunction:: parse_number .. autofunction:: parse_decimal Exceptions ---------- .. autoexception:: NumberFormatError :members: Data Access ----------- .. autofunction:: get_currency_name .. autofunction:: get_currency_symbol .. autofunction:: get_currency_unit_pattern .. autofunction:: get_decimal_symbol .. autofunction:: get_plus_sign_symbol .. autofunction:: get_minus_sign_symbol .. autofunction:: get_territory_currencies babel-2.14.0/docs/api/index.rst0000644000175000017500000000037114536056757015556 0ustar nileshnileshAPI Reference ============= This part of the documentation contains the full API reference of the public API of Babel. .. toctree:: :maxdepth: 2 core dates languages lists messages/index numbers plural support units babel-2.14.0/docs/index.rst0000644000175000017500000000134014536056757015002 0ustar nileshnilesh.. -*- mode: rst; encoding: utf-8 -*- Babel ===== Babel is an integrated collection of utilities that assist in internationalizing and localizing Python applications, with an emphasis on web-based applications. User Documentation ------------------ The user documentation explains some core concept of the library and gives some information about how it can be used. .. toctree:: :maxdepth: 1 intro installation locale dates numbers messages cmdline setup support API Reference ------------- The API reference lists the full public API that Babel provides. .. toctree:: :maxdepth: 2 api/index Additional Notes ---------------- .. toctree:: :maxdepth: 2 dev changelog license babel-2.14.0/.gitignore0000644000175000017500000000055314536056757014206 0ustar nileshnilesh**/__pycache__ *.egg *.egg-info *.pyc *.pyo *.so *.swp *~ .*cache .DS_Store .coverage .idea .tox /venv* babel/global.dat babel/global.dat.json build dist docs/_build test-env tests/messages/data/project/i18n/en_US tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/*.mo tests/messages/data/project/i18n/long_messages.pot tests/messages/data/project/i18n/temp* babel-2.14.0/setup.cfg0000644000175000017500000000063314536056757014036 0ustar nileshnilesh[tool:pytest] norecursedirs = venv* .* _* scripts {args} doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE ALLOW_UNICODE IGNORE_EXCEPTION_DETAIL markers = all_locales: parameterize test with all locales filterwarnings = # The doctest for format_number would raise this, but we don't really want to see it. ignore:babel.numbers.format_decimal:DeprecationWarning [metadata] license_files = LICENSE babel-2.14.0/MANIFEST.in0000644000175000017500000000044314536056757013752 0ustar nileshnileshinclude Makefile CHANGES.rst LICENSE AUTHORS include conftest.py tox.ini include babel/global.dat include babel/locale-data/*.dat recursive-include docs * recursive-exclude docs/_build * include scripts/* recursive-include tests * recursive-exclude tests *.pyc recursive-exclude tests *.pyo babel-2.14.0/CONTRIBUTING.md0000644000175000017500000000412114536056757014442 0ustar nileshnilesh# Babel Contribution Guidelines Welcome to Babel! These guidelines will give you a short overview over how we handle issues and PRs in this repository. Note that they are preliminary and still need proper phrasing - if you'd like to help - be sure to make a PR. Please know that we do appreciate all contributions - bug reports as well as Pull Requests. ## Setting up a development environment and running tests After you've cloned the repository, 1. Set up a Python virtualenv (the methods vary depending on tooling and operating system) and activate it. 2. Install Babel in editable mode with development dependencies: `pip install -e .[dev]` 3. Run `make import-cldr` to import the CLDR database. This will download the CLDR database and convert it to a format that Babel can use. 4. Run `make test` to run the tests. You can also run e.g. `pytest --cov babel .` to run the tests with coverage reporting enabled. You can also use [Tox][tox] to run the tests in separate virtualenvs for all supported Python versions; a `tox.ini` configuration (which is what the CI process uses) is included in the repository. ## On pull requests ### PR Merge Criteria For a PR to be merged, the following statements must hold true: - All CI services pass. (Windows build, linux build, sufficient test coverage.) - All commits must have been reviewed and approved by a babel maintainer who is not the author of the PR. Commits shall comply to the "Good Commits" standards outlined below. To begin contributing have a look at the open [easy issues](https://github.com/python-babel/babel/issues?q=is%3Aopen+is%3Aissue+label%3Adifficulty%2Flow) which could be fixed. ### Correcting PRs Rebasing PRs is preferred over merging master into the source branches again and again cluttering our history. If a reviewer has suggestions, the commit shall be amended so the history is not cluttered by "fixup commits". ### Writing Good Commits Please see https://api.coala.io/en/latest/Developers/Writing_Good_Commits.html for guidelines on how to write good commits and proper commit messages. [tox]: https://tox.wiki/en/latest/ babel-2.14.0/contrib/0000755000175000017500000000000014536056757013653 5ustar nileshnileshbabel-2.14.0/contrib/babel.js0000644000175000017500000001122014536056757015252 0ustar nileshnilesh/** * Babel JavaScript Support * * Copyright (C) 2008-2011 Edgewall Software * All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://babel.edgewall.org/wiki/License. * * This software consists of voluntary contributions made by many * individuals. For the exact contribution history, see the revision * history and logs, available at http://babel.edgewall.org/log/. */ /** * A simple module that provides a gettext like translation interface. * The catalog passed to load() must be an object conforming to this * interface:: * * { * messages: an object of {msgid: translations} items where * translations is an array of messages or a single * string if the message is not pluralizable. * plural_expr: the plural expression for the language. * locale: the identifier for this locale. * domain: the name of the domain. * } * * Missing elements in the object are ignored. * * Typical usage:: * * var translations = babel.Translations.load(...).install(); */ var babel = new function() { var defaultPluralExpr = function(n) { return n == 1 ? 0 : 1; }; var formatRegex = /%?%(?:\(([^\)]+)\))?([disr])/g; /** * A translations object implementing the gettext interface */ var Translations = this.Translations = function(locale, domain) { this.messages = {}; this.locale = locale || 'unknown'; this.domain = domain || 'messages'; this.pluralexpr = defaultPluralExpr; }; /** * Create a new translations object from the catalog and return it. * See the babel-module comment for more details. */ Translations.load = function(catalog) { var rv = new Translations(); rv.load(catalog); return rv; }; Translations.prototype = { /** * translate a single string. */ gettext: function(string) { var translated = this.messages[string]; if (typeof translated == 'undefined') return string; return (typeof translated == 'string') ? translated : translated[0]; }, /** * translate a pluralizable string */ ngettext: function(singular, plural, n) { var translated = this.messages[singular]; if (typeof translated == 'undefined') return (n == 1) ? singular : plural; return translated[this.pluralexpr(n)]; }, /** * Install this translation document wide. After this call, there are * three new methods on the window object: _, gettext and ngettext */ install: function() { var self = this; window._ = window.gettext = function(string) { return self.gettext(string); }; window.ngettext = function(singular, plural, n) { return self.ngettext(singular, plural, n); }; return this; }, /** * Works like Translations.load but updates the instance rather * then creating a new one. */ load: function(catalog) { if (catalog.messages) this.update(catalog.messages); if (catalog.plural_expr) this.setPluralExpr(catalog.plural_expr); if (catalog.locale) this.locale = catalog.locale; if (catalog.domain) this.domain = catalog.domain; return this; }, /** * Updates the translations with the object of messages. */ update: function(mapping) { for (var key in mapping) if (mapping.hasOwnProperty(key)) this.messages[key] = mapping[key]; return this; }, /** * Sets the plural expression */ setPluralExpr: function(expr) { this.pluralexpr = new Function('n', 'return +(' + expr + ')'); return this; } }; /** * A python inspired string formatting function. Supports named and * positional placeholders and "s", "d" and "i" as type characters * without any formatting specifications. * * Examples:: * * babel.format(_('Hello %s'), name) * babel.format(_('Progress: %(percent)s%%'), {percent: 100}) */ this.format = function() { var arg, string = arguments[0], idx = 0; if (arguments.length == 1) return string; else if (arguments.length == 2 && typeof arguments[1] == 'object') arg = arguments[1]; else { arg = []; for (var i = 1, n = arguments.length; i != n; ++i) arg[i - 1] = arguments[i]; } return string.replace(formatRegex, function(all, name, type) { if (all[0] == all[1]) return all.substring(1); var value = arg[name || idx++]; return (type == 'i' || type == 'd') ? +value : value; }); } }; babel-2.14.0/LICENSE0000644000175000017500000000277314536056757013231 0ustar nileshnileshCopyright (c) 2013-2023 by the Babel Team, see AUTHORS for more information. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. babel-2.14.0/CHANGES.rst0000644000175000017500000012101414536056757014014 0ustar nileshnileshBabel Changelog =============== Version 2.14.0 -------------- Upcoming deprecation ~~~~~~~~~~~~~~~~~~~~ * This version, Babel 2.14, is the last version of Babel to support Python 3.7. Babel 2.15 will require Python 3.8 or newer. * We had previously announced Babel 2.13 to have been the last version to support Python 3.7, but being able to use CLDR 43 with Python 3.7 was deemed important enough to keep supporting the EOL Python version for one more release. Possibly backwards incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * ``Locale.number_symbols`` will now have first-level keys for each numbering system. Since the implicit default numbering system still is ``"latn"``, what had previously been e.g. ``Locale.number_symbols['decimal']`` is now ``Locale.number_symbols['latn']['decimal']``. * Babel no longer directly depends on either ``distutils`` or ``setuptools``; if you had been using the Babel setuptools command extensions, you would need to explicitly depend on ``setuptools`` – though given you're running ``setup.py`` you probably already do. Features ~~~~~~~~ * CLDR/Numbers: Add support of local numbering systems for number symbols by @kajte in :gh:`1036` * CLDR: Upgrade to CLDR 43 by @rix0rrr in :gh:`1043` * Frontend: Allow last_translator to be passed as an option to extract_message by @AivGitHub in :gh:`1044` * Frontend: Decouple `pybabel` CLI frontend from distutils/setuptools by @akx in :gh:`1041` * Numbers: Improve parsing of malformed decimals by @Olunusib and @akx in :gh:`1042` Infrastructure ~~~~~~~~~~~~~~ * Enforce trailing commas (enable Ruff COM rule and autofix) by @akx in :gh:`1045` * CI: use GitHub output formats by @akx in :gh:`1046` Version 2.13.1 -------------- This is a patch release to fix a few bugs. Fixes ~~~~~ * Fix a typo in ``_locales_to_names`` by @Dl84 in :gh:`1038` (issue :gh:`1037`) * Fix ``setuptools`` dependency for Python 3.12 by @opryprin in :gh:`1033` Version 2.13.0 -------------- Upcoming deprecation (reverted) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * It was previously announced that this version, Babel 2.13, would be the last version of Babel to support Python 3.7. Babel 2.14 will still support Python 3.7. Features ~~~~~~~~ * Add flag to ignore POT-Creation-Date for updates by @joeportela in :gh:`999` * Support 't' specifier in keywords by @jeanas in :gh:`1015` * Add f-string parsing for Python 3.12 (PEP 701) by @encukou in :gh:`1027` Fixes ~~~~~ * Various typing-related fixes by @akx in :gh:`979`, in :gh:`978`, :gh:`981`, :gh:`983` * babel.messages.catalog: deduplicate _to_fuzzy_match_key logic by @akx in :gh:`980` * Freeze format_time() tests to a specific date to fix test failures by @mgorny in :gh:`998` * Spelling and grammar fixes by @scop in :gh:`1008` * Renovate lint tools by @akx in :gh:`1017`, :gh:`1028` * Use SPDX license identifier by @vargenau in :gh:`994` * Use aware UTC datetimes internally by @scop in :gh:`1009` New Contributors ~~~~~~~~~~~~~~~~ * @mgorny made their first contribution in :gh:`998` * @vargenau made their first contribution in :gh:`994` * @joeportela made their first contribution in :gh:`999` * @encukou made their first contribution in :gh:`1027` Version 2.12.1 -------------- Fixes ~~~~~ * Version 2.12.0 was missing the ``py.typed`` marker file. Thanks to Alex Waygood for the fix! :gh:`975` * The copyright year in all files was bumped to 2023. Version 2.12.0 -------------- Deprecations & breaking changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Python 3.6 is no longer supported (:gh:`919`) - Aarni Koskela * The `get_next_timezone_transition` function is no more (:gh:`958`) - Aarni Koskela * `Locale.parse()` will no longer return `None`; it will always return a Locale or raise an exception. Passing in `None`, though technically allowed by the typing, will raise. (:gh:`966`) New features ~~~~~~~~~~~~ * CLDR: Babel now uses CLDR 42 (:gh:`951`) - Aarni Koskela * Dates: `pytz` is now optional; Babel will prefer it but will use `zoneinfo` when available. (:gh:`940`) - @ds-cbo * General: Babel now ships type annotations, thanks to Jonah Lawrence's work in multiple PRs. * Locales: @modifiers are now retained when parsing locales (:gh:`947`) - martin f. krafft * Messages: JavaScript template string expression extraction is now smarter. (:gh:`939`) - Johannes Wilm * Numbers: NaN and Infinity are now better supported (:gh:`955`) - Jonah Lawrence * Numbers: Short compact currency formats are now supported (:gh:`926`) - Jonah Lawrence * Numbers: There's now a `Format.compact_decimal` utility function. (:gh:`921`) - Jonah Lawrence Bugfixes ~~~~~~~~ * Dates: The cache for parsed datetime patterns is now bounded (:gh:`967`) - Aarni Koskela * Messages: Fuzzy candidate matching accuracy is improved (:gh:`970`) - Jean Abou Samra * Numbers: Compact singular formats and patterns with no numbers work correctly (:gh:`930`, :gh:`932`) - Jonah Lawrence, Jun Omae Improvements & cleanup ~~~~~~~~~~~~~~~~~~~~~~ * Dates: `babel.dates.UTC` is now an alias for `datetime.timezone.utc` (:gh:`957`) - Aarni Koskela * Dates: `babel.localtime` was slightly cleaned up. (:gh:`952`) - Aarni Koskela * Documentation: Documentation was improved by Maciej Olko, Jonah Lawrence, lilinjie, and Aarni Koskela. * Infrastructure: Babel is now being linted with pre-commit and ruff. - Aarni Koskela Version 2.11.0 -------------- Upcoming deprecation ~~~~~~~~~~~~~~~~~~~~ * This version, Babel 2.11, is the last version of Babel to support Python 3.6. Babel 2.12 will require Python 3.7 or newer. Improvements ~~~~~~~~~~~~ * Support for hex escapes in JavaScript string literals :gh:`877` - Przemyslaw Wegrzyn * Add support for formatting decimals in compact form :gh:`909` - Jonah Lawrence * Adapt parse_date to handle ISO dates in ASCII format :gh:`842` - Eric L. * Use `ast` instead of `eval` for Python string extraction :gh:`915` - Aarni Koskela * This also enables extraction from static f-strings. F-strings with expressions are silently ignored (but won't raise an error as they used to). Infrastructure ~~~~~~~~~~~~~~ * Tests: Use regular asserts and ``pytest.raises()`` :gh:`875` – Aarni Koskela * Wheels are now built in GitHub Actions :gh:`888` – Aarni Koskela * Small improvements to the CLDR downloader script :gh:`894` – Aarni Koskela * Remove antiquated `__nonzero__` methods :gh:`896` - Nikita Sobolev * Remove superfluous `__unicode__` declarations :gh:`905` - Lukas Juhrich * Mark package compatible with Python 3.11 :gh:`913` - Aarni Koskela * Quiesce pytest warnings :gh:`916` - Aarni Koskela Bugfixes ~~~~~~~~ * Use email.Message for pofile header parsing instead of the deprecated ``cgi.parse_header`` function. :gh:`876` – Aarni Koskela * Remove determining time zone via systemsetup on macOS :gh:`914` - Aarni Koskela Documentation ~~~~~~~~~~~~~ * Update Python versions in documentation :gh:`898` - Raphael Nestler * Align BSD-3 license with OSI template :gh:`912` - Lukas Kahwe Smith Version 2.10.3 -------------- This is a bugfix release for Babel 2.10.2, which was mistakenly packaged with outdated locale data. Thanks to Michał Górny for pointing this out and Jun Omae for verifying. This and future Babel PyPI packages will be built by a more automated process, which should make problems like this less likely to occur. Version 2.10.2 -------------- This is a bugfix release for Babel 2.10.1. * Fallback count="other" format in format_currency() (:gh:`872`) - Jun Omae * Fix get_period_id() with ``dayPeriodRule`` across 0:00 (:gh:`871`) - Jun Omae * Add support for ``b`` and ``B`` period symbols in time format (:gh:`869`) - Jun Omae * chore(docs/typo): Fixes a minor typo in a function comment (:gh:`864`) - Frank Harrison Version 2.10.1 -------------- This is a bugfix release for Babel 2.10.0. * Messages: Fix ``distutils`` import. Regressed in :gh:`843`. (:gh:`852`) - Nehal J Wani * The wheel file is no longer marked as universal, since Babel only supports Python 3. Version 2.10.0 -------------- Upcoming deprecation ~~~~~~~~~~~~~~~~~~~~ * The ``get_next_timezone_transition()`` function is marked deprecated in this version and will be removed likely as soon as Babel 2.11. No replacement for this function is planned; based on discussion in :gh:`716`, it's likely the function is not used in any real code. (:gh:`852`) - Aarni Koskela, Paul Ganssle Improvements ~~~~~~~~~~~~ * CLDR: Upgrade to CLDR 41.0. (:gh:`853`) - Aarni Koskela * The ``c`` and ``e`` plural form operands introduced in CLDR 40 are parsed, but otherwise unsupported. (:gh:`826`) * Non-nominative forms of units are currently ignored. * Messages: Implement ``--init-missing`` option for ``pybabel update`` (:gh:`785`) - ruro * Messages: For ``extract``, you can now replace the built-in ``.*`` / ``_*`` ignored directory patterns with ones of your own. (:gh:`832`) - Aarni Koskela, Kinshuk Dua * Messages: Add ``--check`` to verify if catalogs are up-to-date (:gh:`831`) - Krzysztof Jagiełło * Messages: Add ``--header-comment`` to override default header comment (:gh:`720`) - Mohamed Hafez Morsy, Aarni Koskela * Dates: ``parse_time`` now supports 12-hour clock, and is better at parsing partial times. (:gh:`834`) - Aarni Koskela, David Bauer, Arthur Jovart * Dates: ``parse_date`` and ``parse_time`` now raise ``ParseError``, a subclass of ``ValueError``, in certain cases. (:gh:`834`) - Aarni Koskela * Dates: ``parse_date`` and ``parse_time`` now accept the ``format`` parameter. (:gh:`834`) - Juliette Monsel, Aarni Koskela Infrastructure ~~~~~~~~~~~~~~ * The internal ``babel/_compat.py`` module is no more (:gh:`808`) - Hugo van Kemenade * Python 3.10 is officially supported (:gh:`809`) - Hugo van Kemenade * There's now a friendly GitHub issue template. (:gh:`800`) – Álvaro Mondéjar Rubio * Don't use the deprecated format_number function internally or in tests - Aarni Koskela * Add GitHub URL for PyPi (:gh:`846`) - Andrii Oriekhov * Python 3.12 compatibility: Prefer setuptools imports to distutils imports (:gh:`843`) - Aarni Koskela * Python 3.11 compatibility: Add deprecations to l*gettext variants (:gh:`835`) - Aarni Koskela * CI: Babel is now tested with PyPy 3.7. (:gh:`851`) - Aarni Koskela Bugfixes ~~~~~~~~ * Date formatting: Allow using ``other`` as fallback form (:gh:`827`) - Aarni Koskela * Locales: ``Locale.parse()`` normalizes variant tags to upper case (:gh:`829`) - Aarni Koskela * A typo in the plural format for Maltese is fixed. (:gh:`796`) - Lukas Winkler * Messages: Catalog date parsing is now timezone independent. (:gh:`701`) - rachele-collin * Messages: Fix duplicate locations when writing without lineno (:gh:`837`) - Sigurd Ljødal * Messages: Fix missing trailing semicolon in plural form headers (:gh:`848`) - farhan5900 * CLI: Fix output of ``--list-locales`` to not be a bytes repr (:gh:`845`) - Morgan Wahl Documentation ~~~~~~~~~~~~~ * Documentation is now correctly built again, and up to date (:gh:`830`) - Aarni Koskela Version 2.9.1 ------------- Bugfixes ~~~~~~~~ * The internal locale-data loading functions now validate the name of the locale file to be loaded and only allow files within Babel's data directory. Thank you to Chris Lyne of Tenable, Inc. for discovering the issue! Version 2.9.0 ------------- Upcoming version support changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * This version, Babel 2.9, is the last version of Babel to support Python 2.7, Python 3.4, and Python 3.5. Improvements ~~~~~~~~~~~~ * CLDR: Use CLDR 37 – Aarni Koskela (:gh:`734`) * Dates: Handle ZoneInfo objects in get_timezone_location, get_timezone_name - Alessio Bogon (:gh:`741`) * Numbers: Add group_separator feature in number formatting - Abdullah Javed Nesar (:gh:`726`) Bugfixes ~~~~~~~~ * Dates: Correct default Format().timedelta format to 'long' to mute deprecation warnings – Aarni Koskela * Import: Simplify iteration code in "import_cldr.py" – Felix Schwarz * Import: Stop using deprecated ElementTree methods "getchildren()" and "getiterator()" – Felix Schwarz * Messages: Fix unicode printing error on Python 2 without TTY. – Niklas Hambüchen * Messages: Introduce invariant that _invalid_pofile() takes unicode line. – Niklas Hambüchen * Tests: fix tests when using Python 3.9 – Felix Schwarz * Tests: Remove deprecated 'sudo: false' from Travis configuration – Jon Dufresne * Tests: Support Py.test 6.x – Aarni Koskela * Utilities: LazyProxy: Handle AttributeError in specified func – Nikiforov Konstantin (:gh:`724`) * Utilities: Replace usage of parser.suite with ast.parse – Miro Hrončok Documentation ~~~~~~~~~~~~~ * Update parse_number comments – Brad Martin (:gh:`708`) * Add __iter__ to Catalog documentation – @CyanNani123 Version 2.8.1 ------------- This is solely a patch release to make running tests on Py.test 6+ possible. Bugfixes ~~~~~~~~ * Support Py.test 6 - Aarni Koskela (:gh:`747`, :gh:`750`, :gh:`752`) Version 2.8.0 ------------- Improvements ~~~~~~~~~~~~ * CLDR: Upgrade to CLDR 36.0 - Aarni Koskela (:gh:`679`) * Messages: Don't even open files with the "ignore" extraction method - @sebleblanc (:gh:`678`) Bugfixes ~~~~~~~~ * Numbers: Fix formatting very small decimals when quantization is disabled - Lev Lybin, @miluChen (:gh:`662`) * Messages: Attempt to sort all messages – Mario Frasca (:gh:`651`, :gh:`606`) Docs ~~~~ * Add years to changelog - Romuald Brunet * Note that installation requires pytz - Steve (Gadget) Barnes Version 2.7.0 ------------- Possibly incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These may be backward incompatible in some cases, as some more-or-less internal APIs have changed. Please feel free to file issues if you bump into anything strange and we'll try to help! * General: Internal uses of ``babel.util.odict`` have been replaced with ``collections.OrderedDict`` from The Python standard library. Improvements ~~~~~~~~~~~~ * CLDR: Upgrade to CLDR 35.1 - Alberto Mardegan, Aarni Koskela (:gh:`626`, :gh:`643`) * General: allow anchoring path patterns to the start of a string - Brian Cappello (:gh:`600`) * General: Bumped version requirement on pytz - @chrisbrake (:gh:`592`) * Messages: `pybabel compile`: exit with code 1 if errors were encountered - Aarni Koskela (:gh:`647`) * Messages: Add omit-header to update_catalog - Cédric Krier (:gh:`633`) * Messages: Catalog update: keep user comments from destination by default - Aarni Koskela (:gh:`648`) * Messages: Skip empty message when writing mo file - Cédric Krier (:gh:`564`) * Messages: Small fixes to avoid crashes on badly formatted .po files - Bryn Truscott (:gh:`597`) * Numbers: `parse_decimal()` `strict` argument and `suggestions` - Charly C (:gh:`590`) * Numbers: don't repeat suggestions in parse_decimal strict - Serban Constantin (:gh:`599`) * Numbers: implement currency formatting with long display names - Luke Plant (:gh:`585`) * Numbers: parse_decimal(): assume spaces are equivalent to non-breaking spaces when not in strict mode - Aarni Koskela (:gh:`649`) * Performance: Cache locale_identifiers() - Aarni Koskela (:gh:`644`) Bugfixes ~~~~~~~~ * CLDR: Skip alt=... for week data (minDays, firstDay, weekendStart, weekendEnd) - Aarni Koskela (:gh:`634`) * Dates: Fix wrong weeknumber for 31.12.2018 - BT-sschmid (:gh:`621`) * Locale: Avoid KeyError trying to get data on WindowsXP - mondeja (:gh:`604`) * Locale: get_display_name(): Don't attempt to concatenate variant information to None - Aarni Koskela (:gh:`645`) * Messages: pofile: Add comparison operators to _NormalizedString - Aarni Koskela (:gh:`646`) * Messages: pofile: don't crash when message.locations can't be sorted - Aarni Koskela (:gh:`646`) Tooling & docs ~~~~~~~~~~~~~~ * Docs: Remove all references to deprecated easy_install - Jon Dufresne (:gh:`610`) * Docs: Switch print statement in docs to print function - NotAFile * Docs: Update all pypi.python.org URLs to pypi.org - Jon Dufresne (:gh:`587`) * Docs: Use https URLs throughout project where available - Jon Dufresne (:gh:`588`) * Support: Add testing and document support for Python 3.7 - Jon Dufresne (:gh:`611`) * Support: Test on Python 3.8-dev - Aarni Koskela (:gh:`642`) * Support: Using ABCs from collections instead of collections.abc is deprecated. - Julien Palard (:gh:`609`) * Tests: Fix conftest.py compatibility with pytest 4.3 - Miro Hrončok (:gh:`635`) * Tests: Update pytest and pytest-cov - Miro Hrončok (:gh:`635`) Version 2.6.0 ------------- Possibly incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These may be backward incompatible in some cases, as some more-or-less internal APIs have changed. Please feel free to file issues if you bump into anything strange and we'll try to help! * Numbers: Refactor decimal handling code and allow bypass of decimal quantization. (@kdeldycke) (PR :gh:`538`) * Messages: allow processing files that are in locales unknown to Babel (@akx) (PR :gh:`557`) * General: Drop support for EOL Python 2.6 and 3.3 (@hugovk) (PR :gh:`546`) Other changes ~~~~~~~~~~~~~ * CLDR: Use CLDR 33 (@akx) (PR :gh:`581`) * Lists: Add support for various list styles other than the default (@akx) (:gh:`552`) * Messages: Add new PoFileError exception (@Bedrock02) (PR :gh:`532`) * Times: Simplify Linux distro specific explicit timezone setting search (@scop) (PR :gh:`528`) Bugfixes ~~~~~~~~ * CLDR: avoid importing alt=narrow currency symbols (@akx) (PR :gh:`558`) * CLDR: ignore non-Latin numbering systems (@akx) (PR :gh:`579`) * Docs: Fix improper example for date formatting (@PTrottier) (PR :gh:`574`) * Tooling: Fix some deprecation warnings (@akx) (PR :gh:`580`) Tooling & docs ~~~~~~~~~~~~~~ * Add explicit signatures to some date autofunctions (@xmo-odoo) (PR :gh:`554`) * Include license file in the generated wheel package (@jdufresne) (PR :gh:`539`) * Python 3.6 invalid escape sequence deprecation fixes (@scop) (PR :gh:`528`) * Test and document all supported Python versions (@jdufresne) (PR :gh:`540`) * Update copyright header years and authors file (@akx) (PR :gh:`559`) Version 2.5.3 ------------- This is a maintenance release that reverts undesired API-breaking changes that slipped into 2.5.2 (see :gh:`550`). It is based on v2.5.1 (f29eccd) with commits 7cedb84, 29da2d2 and edfb518 cherry-picked on top. Version 2.5.2 ------------- Bugfixes ~~~~~~~~ * Revert the unnecessary PyInstaller fixes from 2.5.0 and 2.5.1 (:gh:`533`) (@yagebu) Version 2.5.1 ------------- Minor Improvements and bugfixes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Use a fixed datetime to avoid test failures (:gh:`520`) (@narendravardi) * Parse multi-line __future__ imports better (:gh:`519`) (@akx) * Fix validate_currency docstring (:gh:`522`) * Allow normalize_locale and exists to handle various unexpected inputs (:gh:`523`) (@suhojm) * Make PyInstaller support more robust (:gh:`525`, :gh:`526`) (@thijstriemstra, @akx) Version 2.5.0 ------------- New Features ~~~~~~~~~~~~ * Numbers: Add currency utilities and helpers (:gh:`491`) (@kdeldycke) * Support PyInstaller (:gh:`500`, :gh:`505`) (@wodo) Minor Improvements and bugfixes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Dates: Add __str__ to DateTimePattern (:gh:`515`) (@sfermigier) * Dates: Fix an invalid string to bytes comparison when parsing TZ files on Py3 (:gh:`498`) (@rowillia) * Dates: Formatting zero-padded components of dates is faster (:gh:`517`) (@akx) * Documentation: Fix "Good Commits" link in CONTRIBUTING.md (:gh:`511`) (@naryanacharya6) * Documentation: Fix link to Python gettext module (:gh:`512`) (@Linkid) * Messages: Allow both dash and underscore separated locale identifiers in pofiles (:gh:`489`, :gh:`490`) (@akx) * Messages: Extract Python messages in nested gettext calls (:gh:`488`) (@sublee) * Messages: Fix in-place editing of dir list while iterating (:gh:`476`, :gh:`492`) (@MarcDufresne) * Messages: Stabilize sort order (:gh:`482`) (@xavfernandez) * Time zones: Honor the no-inherit marker for metazone names (:gh:`405`) (@akx) Version 2.4.0 ------------- New Features ~~~~~~~~~~~~ Some of these changes might break your current code and/or tests. * CLDR: CLDR 29 is now used instead of CLDR 28 (:gh:`405`) (@akx) * Messages: Add option 'add_location' for location line formatting (:gh:`438`, :gh:`459`) (@rrader, @alxpy) * Numbers: Allow full control of decimal behavior (:gh:`410`) (@etanol) Minor Improvements and bugfixes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Documentation: Improve Date Fields descriptions (:gh:`450`) (@ldwoolley) * Documentation: Typo fixes and documentation improvements (:gh:`406`, :gh:`412`, :gh:`403`, :gh:`440`, :gh:`449`, :gh:`463`) (@zyegfryed, @adamchainz, @jwilk, @akx, @roramirez, @abhishekcs10) * Messages: Default to UTF-8 source encoding instead of ISO-8859-1 (:gh:`399`) (@asottile) * Messages: Ensure messages are extracted in the order they were passed in (:gh:`424`) (@ngrilly) * Messages: Message extraction for JSX files is improved (:gh:`392`, :gh:`396`, :gh:`425`) (@karloskar, @georgschoelly) * Messages: PO file reading supports multi-line obsolete units (:gh:`429`) (@mbirtwell) * Messages: Python message extractor respects unicode_literals in __future__ (:gh:`427`) (@sublee) * Messages: Roundtrip Language headers (:gh:`420`) (@kruton) * Messages: units before obsolete units are no longer erroneously marked obsolete (:gh:`452`) (@mbirtwell) * Numbers: `parse_pattern` now preserves the full original pattern (:gh:`414`) (@jtwang) * Numbers: Fix float conversion in `extract_operands` (:gh:`435`) (@akx) * Plurals: Fix plural forms for Czech and Slovak locales (:gh:`373`) (@ykshatroff) * Plurals: More plural form fixes based on Mozilla and CLDR references (:gh:`431`) (@mshenfield) Internal improvements ~~~~~~~~~~~~~~~~~~~~~ * Local times are constructed correctly in tests (:gh:`411`) (@etanol) * Miscellaneous small improvements (:gh:`437`) (@scop) * Regex flags are extracted from the regex strings (:gh:`462`) (@singingwolfboy) * The PO file reader is now a class and has seen some refactoring (:gh:`429`, :gh:`452`) (@mbirtwell) Version 2.3.4 ------------- (Bugfix release, released on April 22th 2016) Bugfixes ~~~~~~~~ * CLDR: The lxml library is no longer used for CLDR importing, so it should not cause strange failures either. Thanks to @aronbierbaum for the bug report and @jtwang for the fix. (:gh:`393`) * CLI: Every last single CLI usage regression should now be gone, and both distutils and stand-alone CLIs should work as they have in the past. Thanks to @paxswill and @ajaeger for bug reports. (:gh:`389`) Version 2.3.3 ------------- (Bugfix release, released on April 12th 2016) Bugfixes ~~~~~~~~ * CLI: Usage regressions that had snuck in between 2.2 and 2.3 should be no more. (:gh:`386`) Thanks to @ajaeger, @sebdiem and @jcristovao for bug reports and patches. Version 2.3.2 ------------- (Bugfix release, released on April 9th 2016) Bugfixes ~~~~~~~~ * Dates: Period (am/pm) formatting was broken in certain locales (namely zh_TW). Thanks to @jun66j5 for the bug report. (:gh:`378`, :gh:`379`) Version 2.3.1 ------------- (Bugfix release because of deployment problems, released on April 8th 2016) Version 2.3 ----------- (Feature release, released on April 8th 2016) Internal improvements ~~~~~~~~~~~~~~~~~~~~~ * The CLI frontend and Distutils commands use a shared implementation (:gh:`311`) * PyPy3 is supported (:gh:`343`) Features ~~~~~~~~ * CLDR: Add an API for territory language data (:gh:`315`) * Core: Character order and measurement system data is imported and exposed (:gh:`368`) * Dates: Add an API for time interval formatting (:gh:`316`) * Dates: More pattern formats and lengths are supported (:gh:`347`) * Dates: Period IDs are imported and exposed (:gh:`349`) * Dates: Support for date-time skeleton formats has been added (:gh:`265`) * Dates: Timezone formatting has been improved (:gh:`338`) * Messages: JavaScript extraction now supports dotted names, ES6 template strings and JSX tags (:gh:`332`) * Messages: npgettext is recognized by default (:gh:`341`) * Messages: The CLI learned to accept multiple domains (:gh:`335`) * Messages: The extraction commands now accept filenames in addition to directories (:gh:`324`) * Units: A new API for unit formatting is implemented (:gh:`369`) Bugfixes ~~~~~~~~ * Core: Mixed-case locale IDs work more reliably (:gh:`361`) * Dates: S...S formats work correctly now (:gh:`360`) * Messages: All messages are now sorted correctly if sorting has been specified (:gh:`300`) * Messages: Fix the unexpected behavior caused by catalog header updating (e0e7ef1) (:gh:`320`) * Messages: Gettext operands are now generated correctly (:gh:`295`) * Messages: Message extraction has been taught to detect encodings better (:gh:`274`) Version 2.2 ----------- (Feature release, released on January 2nd 2016) Bugfixes ~~~~~~~~ * General: Add __hash__ to Locale. (:gh:`303`) (2aa8074) * General: Allow files with BOM if they're UTF-8 (:gh:`189`) (da87edd) * General: localedata directory is now locale-data (:gh:`109`) (2d1882e) * General: odict: Fix pop method (0a9e97e) * General: Removed uses of datetime.date class from .dat files (:gh:`174`) (94f6830) * Messages: Fix plural selection for Chinese (531f666) * Messages: Fix typo and add semicolon in plural_forms (5784501) * Messages: Flatten NullTranslations.files into a list (ad11101) * Times: FixedOffsetTimezone: fix display of negative offsets (d816803) Features ~~~~~~~~ * CLDR: Update to CLDR 28 (:gh:`292`) (9f7f4d0) * General: Add __copy__ and __deepcopy__ to LazyProxy. (a1cc3f1) * General: Add official support for Python 3.4 and 3.5 * General: Improve odict performance by making key search O(1) (6822b7f) * Locale: Add an ordinal_form property to Locale (:gh:`270`) (b3f3430) * Locale: Add support for list formatting (37ce4fa, be6e23d) * Locale: Check inheritance exceptions first (3ef0d6d) * Messages: Allow file locations without line numbers (:gh:`279`) (79bc781) * Messages: Allow passing a callable to `extract()` (:gh:`289`) (3f58516) * Messages: Support 'Language' header field of PO files (:gh:`76`) (3ce842b) * Messages: Update catalog headers from templates (e0e7ef1) * Numbers: Properly load and expose currency format types (:gh:`201`) (df676ab) * Numbers: Use cdecimal by default when available (b6169be) * Numbers: Use the CLDR's suggested number of decimals for format_currency (:gh:`139`) (201ed50) * Times: Add format_timedelta(format='narrow') support (edc5eb5) Version 2.1 ----------- (Bugfix/minor feature release, released on September 25th 2015) - Parse and honour the locale inheritance exceptions (:gh:`97`) - Fix Locale.parse using ``global.dat`` incompatible types (:gh:`174`) - Fix display of negative offsets in ``FixedOffsetTimezone`` (:gh:`214`) - Improved odict performance which is used during localization file build, should improve compilation time for large projects - Add support for "narrow" format for ``format_timedelta`` - Add universal wheel support - Support 'Language' header field in .PO files (fixes :gh:`76`) - Test suite enhancements (coverage, broken tests fixed, etc) - Documentation updated Version 2.0 ----------- (Released on July 27th 2015, codename Second Coming) - Added support for looking up currencies that belong to a territory through the :func:`babel.numbers.get_territory_currencies` function. - Improved Python 3 support. - Fixed some broken tests for timezone behavior. - Improved various smaller things for dealing with dates. Version 1.4 ----------- (bugfix release, release date to be decided) - Fixed a bug that caused deprecated territory codes not being converted properly by the subtag resolving. This for instance showed up when trying to use ``und_UK`` as a language code which now properly resolves to ``en_GB``. - Fixed a bug that made it impossible to import the CLDR data from scratch on windows systems. Version 1.3 ----------- (bugfix release, released on July 29th 2013) - Fixed a bug in likely-subtag resolving for some common locales. This primarily makes ``zh_CN`` work again which was broken due to how it was defined in the likely subtags combined with our broken resolving. This fixes :gh:`37`. - Fixed a bug that caused pybabel to break when writing to stdout on Python 3. - Removed a stray print that was causing issues when writing to stdout for message catalogs. Version 1.2 ----------- (bugfix release, released on July 27th 2013) - Included all tests in the tarball. Previously the include skipped past recursive folders. - Changed how tests are invoked and added separate standalone test command. This simplifies testing of the package for linux distributors. Version 1.1 ----------- (bugfix release, released on July 27th 2013) - added dummy version requirements for pytz so that it installs on pip 1.4. - Included tests in the tarball. Version 1.0 ----------- (Released on July 26th 2013, codename Revival) - support python 2.6, 2.7, 3.3+ and pypy - drop all other versions - use tox for testing on different pythons - Added support for the locale plural rules defined by the CLDR. - Added `format_timedelta` function to support localized formatting of relative times with strings such as "2 days" or "1 month" (:trac:`126`). - Fixed negative offset handling of Catalog._set_mime_headers (:trac:`165`). - Fixed the case where messages containing square brackets would break with an unpack error. - updated to CLDR 23 - Make the CLDR import script work with Python 2.7. - Fix various typos. - Sort output of list-locales. - Make the POT-Creation-Date of the catalog being updated equal to POT-Creation-Date of the template used to update (:trac:`148`). - Use a more explicit error message if no option or argument (command) is passed to pybabel (:trac:`81`). - Keep the PO-Revision-Date if it is not the default value (:trac:`148`). - Make --no-wrap work by reworking --width's default and mimic xgettext's behaviour of always wrapping comments (:trac:`145`). - Add --project and --version options for commandline (:trac:`173`). - Add a __ne__() method to the Local class. - Explicitly sort instead of using sorted() and don't assume ordering (Jython compatibility). - Removed ValueError raising for string formatting message checkers if the string does not contain any string formatting (:trac:`150`). - Fix Serbian plural forms (:trac:`213`). - Small speed improvement in format_date() (:trac:`216`). - Fix so frontend.CommandLineInterface.run does not accumulate logging handlers (:trac:`227`, reported with initial patch by dfraser) - Fix exception if environment contains an invalid locale setting (:trac:`200`) - use cPickle instead of pickle for better performance (:trac:`225`) - Only use bankers round algorithm as a tie breaker if there are two nearest numbers, round as usual if there is only one nearest number (:trac:`267`, patch by Martin) - Allow disabling cache behaviour in LazyProxy (:trac:`208`, initial patch from Pedro Algarvio) - Support for context-aware methods during message extraction (:trac:`229`, patch from David Rios) - "init" and "update" commands support "--no-wrap" option (:trac:`289`) - fix formatting of fraction in format_decimal() if the input value is a float with more than 7 significant digits (:trac:`183`) - fix format_date() with datetime parameter (:trac:`282`, patch from Xavier Morel) - fix format_decimal() with small Decimal values (:trac:`214`, patch from George Lund) - fix handling of messages containing '\\n' (:trac:`198`) - handle irregular multi-line msgstr (no "" as first line) gracefully (:trac:`171`) - parse_decimal() now returns Decimals not floats, API change (:trac:`178`) - no warnings when running setup.py without installed setuptools (:trac:`262`) - modified Locale.__eq__ method so Locales are only equal if all of their attributes (language, territory, script, variant) are equal - resort to hard-coded message extractors/checkers if pkg_resources is installed but no egg-info was found (:trac:`230`) - format_time() and format_datetime() now accept also floats (:trac:`242`) - add babel.support.NullTranslations class similar to gettext.NullTranslations but with all of Babel's new gettext methods (:trac:`277`) - "init" and "update" commands support "--width" option (:trac:`284`) - fix 'input_dirs' option for setuptools integration (:trac:`232`, initial patch by Étienne Bersac) - ensure .mo file header contains the same information as the source .po file (:trac:`199`) - added support for get_language_name() on the locale objects. - added support for get_territory_name() on the locale objects. - added support for get_script_name() on the locale objects. - added pluralization support for currency names and added a '¤¤¤' pattern for currencies that includes the full name. - depend on pytz now and wrap it nicer. This gives us improved support for things like timezone transitions and an overall nicer API. - Added support for explicit charset to PO file reading. - Added experimental Python 3 support. - Added better support for returning timezone names. - Don't throw away a Catalog's obsolete messages when updating it. - Added basic likelySubtag resolving when doing locale parsing and no match can be found. Version 0.9.6 ------------- (released on March 17th 2011) - Backport r493-494: documentation typo fixes. - Make the CLDR import script work with Python 2.7. - Fix various typos. - Fixed Python 2.3 compatibility (:trac:`146`, :trac:`233`). - Sort output of list-locales. - Make the POT-Creation-Date of the catalog being updated equal to POT-Creation-Date of the template used to update (:trac:`148`). - Use a more explicit error message if no option or argument (command) is passed to pybabel (:trac:`81`). - Keep the PO-Revision-Date if it is not the default value (:trac:`148`). - Make --no-wrap work by reworking --width's default and mimic xgettext's behaviour of always wrapping comments (:trac:`145`). - Fixed negative offset handling of Catalog._set_mime_headers (:trac:`165`). - Add --project and --version options for commandline (:trac:`173`). - Add a __ne__() method to the Local class. - Explicitly sort instead of using sorted() and don't assume ordering (Python 2.3 and Jython compatibility). - Removed ValueError raising for string formatting message checkers if the string does not contain any string formatting (:trac:`150`). - Fix Serbian plural forms (:trac:`213`). - Small speed improvement in format_date() (:trac:`216`). - Fix number formatting for locales where CLDR specifies alt or draft items (:trac:`217`) - Fix bad check in format_time (:trac:`257`, reported with patch and tests by jomae) - Fix so frontend.CommandLineInterface.run does not accumulate logging handlers (:trac:`227`, reported with initial patch by dfraser) - Fix exception if environment contains an invalid locale setting (:trac:`200`) Version 0.9.5 ------------- (released on April 6th 2010) - Fixed the case where messages containing square brackets would break with an unpack error. - Backport of r467: Fuzzy matching regarding plurals should *NOT* be checked against len(message.id) because this is always 2, instead, it's should be checked against catalog.num_plurals (:trac:`212`). Version 0.9.4 ------------- (released on August 25th 2008) - Currency symbol definitions that is defined with choice patterns in the CLDR data are no longer imported, so the symbol code will be used instead. - Fixed quarter support in date formatting. - Fixed a serious memory leak that was introduces by the support for CLDR aliases in 0.9.3 (:trac:`128`). - Locale modifiers such as "@euro" are now stripped from locale identifiers when parsing (:trac:`136`). - The system locales "C" and "POSIX" are now treated as aliases for "en_US_POSIX", for which the CLDR provides the appropriate data. Thanks to Manlio Perillo for the suggestion. - Fixed JavaScript extraction for regular expression literals (:trac:`138`) and concatenated strings. - The `Translation` class in `babel.support` can now manage catalogs with different message domains, and exposes the family of `d*gettext` functions (:trac:`137`). Version 0.9.3 ------------- (released on July 9th 2008) - Fixed invalid message extraction methods causing an UnboundLocalError. - Extraction method specification can now use a dot instead of the colon to separate module and function name (:trac:`105`). - Fixed message catalog compilation for locales with more than two plural forms (:trac:`95`). - Fixed compilation of message catalogs for locales with more than two plural forms where the translations were empty (:trac:`97`). - The stripping of the comment tags in comments is optional now and is done for each line in a comment. - Added a JavaScript message extractor. - Updated to CLDR 1.6. - Fixed timezone calculations when formatting datetime and time values. - Added a `get_plural` function into the plurals module that returns the correct plural forms for a locale as tuple. - Added support for alias definitions in the CLDR data files, meaning that the chance for items missing in certain locales should be greatly reduced (:trac:`68`). Version 0.9.2 ------------- (released on February 4th 2008) - Fixed catalogs' charset values not being recognized (:trac:`66`). - Numerous improvements to the default plural forms. - Fixed fuzzy matching when updating message catalogs (:trac:`82`). - Fixed bug in catalog updating, that in some cases pulled in translations from different catalogs based on the same template. - Location lines in PO files do no longer get wrapped at hyphens in file names (:trac:`79`). - Fixed division by zero error in catalog compilation on empty catalogs (:trac:`60`). Version 0.9.1 ------------- (released on September 7th 2007) - Fixed catalog updating when a message is merged that was previously simple but now has a plural form, for example by moving from `gettext` to `ngettext`, or vice versa. - Fixed time formatting for 12 am and 12 pm. - Fixed output encoding of the `pybabel --list-locales` command. - MO files are now written in binary mode on windows (:trac:`61`). Version 0.9 ----------- (released on August 20th 2007) - The `new_catalog` distutils command has been renamed to `init_catalog` for consistency with the command-line frontend. - Added compilation of message catalogs to MO files (:trac:`21`). - Added updating of message catalogs from POT files (:trac:`22`). - Support for significant digits in number formatting. - Apply proper "banker's rounding" in number formatting in a cross-platform manner. - The number formatting functions now also work with numbers represented by Python `Decimal` objects (:trac:`53`). - Added extensible infrastructure for validating translation catalogs. - Fixed the extractor not filtering out messages that didn't validate against the keyword's specification (:trac:`39`). - Fixed the extractor raising an exception when encountering an empty string msgid. It now emits a warning to stderr. - Numerous Python message extractor fixes: it now handles nested function calls within a gettext function call correctly, uses the correct line number for multi-line function calls, and other small fixes (tickets :trac:`38` and :trac:`39`). - Improved support for detecting Python string formatting fields in message strings (:trac:`57`). - CLDR upgraded to the 1.5 release. - Improved timezone formatting. - Implemented scientific number formatting. - Added mechanism to lookup locales by alias, for cases where browsers insist on including only the language code in the `Accept-Language` header, and sometimes even the incorrect language code. Version 0.8.1 ------------- (released on July 2nd 2007) - `default_locale()` would fail when the value of the `LANGUAGE` environment variable contained multiple language codes separated by colon, as is explicitly allowed by the GNU gettext tools. As the `default_locale()` function is called at the module level in some modules, this bug would completely break importing these modules on systems where `LANGUAGE` is set that way. - The character set specified in PO template files is now respected when creating new catalog files based on that template. This allows the use of characters outside the ASCII range in POT files (:trac:`17`). - The default ordering of messages in generated POT files, which is based on the order those messages are found when walking the source tree, is no longer subject to differences between platforms; directory and file names are now always sorted alphabetically. - The Python message extractor now respects the special encoding comment to be able to handle files containing non-ASCII characters (:trac:`23`). - Added ``N_`` (gettext noop) to the extractor's default keywords. - Made locale string parsing more robust, and also take the script part into account (:trac:`27`). - Added a function to list all locales for which locale data is available. - Added a command-line option to the `pybabel` command which prints out all available locales (:trac:`24`). - The name of the command-line script has been changed from just `babel` to `pybabel` to avoid a conflict with the OpenBabel project (:trac:`34`). Version 0.8 ----------- (released on June 20th 2007) - First public release babel-2.14.0/README.rst0000644000175000017500000000147514536056757013711 0ustar nileshnileshAbout Babel =========== Babel is a Python library that provides an integrated collection of utilities that assist with internationalizing and localizing Python applications (in particular web-based applications.) Details can be found in the HTML files in the ``docs`` folder. For more information please visit the Babel web site: http://babel.pocoo.org/ Join the chat at https://gitter.im/python-babel/babel Contributing to Babel ===================== If you want to contribute code to Babel, please take a look at our `CONTRIBUTING.md `__. If you know your way around Babels codebase a bit and like to help further, we would appreciate any help in reviewing pull requests. Please contact us at https://gitter.im/python-babel/babel if you're interested! babel-2.14.0/conftest.py0000644000175000017500000000116714536056757014417 0ustar nileshnileshfrom pathlib import Path from _pytest.doctest import DoctestModule collect_ignore = [ 'babel/messages/setuptools_frontend.py', 'setup.py', 'tests/messages/data', ] babel_path = Path(__file__).parent / 'babel' # Via the stdlib implementation of Path.is_relative_to in Python 3.9 def _is_relative(p1: Path, p2: Path) -> bool: try: p1.relative_to(p2) return True except ValueError: return False def pytest_collect_file(file_path: Path, parent): if _is_relative(file_path, babel_path) and file_path.suffix == '.py': return DoctestModule.from_parent(parent, path=file_path) babel-2.14.0/Makefile0000644000175000017500000000071614536056757013657 0ustar nileshnileshtest: import-cldr python ${PYTHON_TEST_FLAGS} -m pytest ${PYTEST_FLAGS} clean: clean-cldr clean-pyc import-cldr: python scripts/download_import_cldr.py clean-cldr: rm -f babel/locale-data/*.dat rm -f babel/global.dat clean-pyc: find . -name '*.pyc' -exec rm {} \; find . -name '__pycache__' -type d | xargs rm -rf develop: pip install --editable . tox-test: tox .PHONY: test develop tox-test clean-pyc clean-cldr import-cldr clean standalone-test babel-2.14.0/.pre-commit-config.yaml0000644000175000017500000000126714536056757016502 0ustar nileshnileshrepos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.291 hooks: - id: ruff args: - --fix - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: check-added-large-files - id: check-docstring-first exclude: (docs/conf.py) - id: check-json - id: check-yaml - id: debug-statements exclude: (tests/messages/data/) - id: end-of-file-fixer exclude: (tests/messages/data/) - id: name-tests-test args: [ '--django' ] exclude: (tests/messages/data/|.*(consts|utils).py) - id: requirements-txt-fixer - id: trailing-whitespace babel-2.14.0/.readthedocs.yml0000644000175000017500000000106514536056757015303 0ustar nileshnilesh# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.11" jobs: pre_build: # Replace any Babel version something may have pulled in # with the copy we're working on. We'll also need to build # the data files at that point, or date formatting _within_ # Sphinx will fail. - pip install -e . - make import-cldr sphinx: configuration: docs/conf.py formats: - epub - pdf python: install: - requirements: docs/requirements.txt babel-2.14.0/setup.py0000755000175000017500000000716014536056757013734 0ustar nileshnileshimport subprocess import sys from setuptools import Command, setup try: from babel import __version__ except SyntaxError as exc: sys.stderr.write(f"Unable to import Babel ({exc}). Are you running a supported version of Python?\n") sys.exit(1) class import_cldr(Command): description = 'imports and converts the CLDR data' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): subprocess.check_call([sys.executable, 'scripts/download_import_cldr.py']) setup( name='Babel', version=__version__, description='Internationalization utilities', long_description='A collection of tools for internationalizing Python applications.', author='Armin Ronacher', author_email='armin.ronacher@active-4.com', maintainer='Aarni Koskela', maintainer_email='akx@iki.fi', license='BSD-3-Clause', url='https://babel.pocoo.org/', project_urls={ 'Source': 'https://github.com/python-babel/babel', }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ], python_requires='>=3.7', packages=['babel', 'babel.messages', 'babel.localtime'], package_data={"babel": ["py.typed"]}, include_package_data=True, install_requires=[ # This version identifier is currently necessary as # pytz otherwise does not install on pip 1.4 or # higher. # Python 3.9 and later include zoneinfo which replaces pytz 'pytz>=2015.7; python_version<"3.9"', ], extras_require={ 'dev': [ 'pytest>=6.0', 'pytest-cov', 'freezegun~=1.0', ], }, cmdclass={'import_cldr': import_cldr}, zip_safe=False, # Note when adding extractors: builtin extractors we also want to # work if packages are not installed to simplify testing. If you # add an extractor here also manually add it to the "extract" # function in babel.messages.extract. entry_points=""" [console_scripts] pybabel = babel.messages.frontend:main [distutils.commands] compile_catalog = babel.messages.setuptools_frontend:compile_catalog extract_messages = babel.messages.setuptools_frontend:extract_messages init_catalog = babel.messages.setuptools_frontend:init_catalog update_catalog = babel.messages.setuptools_frontend:update_catalog [distutils.setup_keywords] message_extractors = babel.messages.setuptools_frontend:check_message_extractors [babel.checkers] num_plurals = babel.messages.checkers:num_plurals python_format = babel.messages.checkers:python_format [babel.extractors] ignore = babel.messages.extract:extract_nothing python = babel.messages.extract:extract_python javascript = babel.messages.extract:extract_javascript """, ) babel-2.14.0/AUTHORS0000644000175000017500000000575014536056757013272 0ustar nileshnilesh Babel is written and maintained by the Babel team and various contributors: - Aarni Koskela - Christopher Lenz - Armin Ronacher - Alex Morega - Lasse Schuirmann - Felix Schwarz - Pedro Algarvio - Jeroen Ruigrok van der Werven - Philip Jenvey - benselme - Isaac Jurado - Tobias Bieniek - Erick Wilder - Jonah Lawrence - Michael Birtwell - Jonas Borgström - Kevin Deldycke - Ville Skyttä - Jon Dufresne - Jun Omae - Hugo - Heungsub Lee - Jakob Schnitzer - Sachin Paliwal - Alex Willmer - Daniel Neuhäuser - Hugo van Kemenade - Miro Hrončok - Cédric Krier - Luke Plant - Jennifer Wang - Lukas Balaga - sudheesh001 - Jean Abou Samra - Niklas Hambüchen - Changaco - Xavier Fernandez - KO. Mattsson - Sébastien Diemer - alexbodn@gmail.com - saurabhiiit - srisankethu - Erik Romijn - Lukas B - Ryan J Ollos - Arturas Moskvinas - Leonardo Pistone - Hyunjun Kim - Best Olunusi - Teo - Ivan Koldakov - Rico Hermans - Daniel - Oleh Prypin - Petr Viktorin - Jean Abou-Samra - Joe Portela - Marc-Etienne Vargenau - Michał Górny - Alex Waygood - Maciej Olko - martin f. krafft - DS/Charlie - lilinjie - Johannes Wilm - Eric L - Przemyslaw Wegrzyn - Lukas Kahwe Smith - Lukas Juhrich - Nikita Sobolev - Raphael Nestler - Frank Harrison - Nehal J Wani - Mohamed Morsy - Krzysztof Jagiełło - Morgan Wahl - farhan5900 - Sigurd Ljødal - Andrii Oriekhov - rachele-collin - Lukas Winkler - Juliette Monsel - Álvaro Mondéjar Rubio - ruro - Alessio Bogon - Nikiforov Konstantin - Abdullah Javed Nesar - Brad Martin - Tyler Kennedy - CyanNani123 - sebleblanc - He Chen - Steve (Gadget) Barnes - Romuald Brunet - Mario Frasca - BT-sschmid - Alberto Mardegan - mondeja - NotAFile - Julien Palard - Brian Cappello - Serban Constantin - Bryn Truscott - Chris - Charly C - PTrottier - xmo-odoo - StevenJ - Jungmo Ku - Simeon Visser - Narendra Vardi - Stefane Fermigier - Narayan Acharya - François Magimel - Wolfgang Doll - Roy Williams - Marc-André Dufresne - Abhishek Tiwari - David Baumgold - Alex Kuzmenko - Georg Schölly - ldwoolley - Rodrigo Ramírez Norambuena - Jakub Wilk - Roman Rader - Max Shenfield - Nicolas Grilly - Kenny Root - Adam Chainz - Sébastien Fievet - Anthony Sottile - Yuriy Shatrov - iamshubh22 - Sven Anderson - Eoin Nugent - Roman Imankulov - David Stanek - Roy Wellington Ⅳ - Florian Schulze - Todd M. Guerra - Joseph Breihan - Craig Loftus - The Gitter Badger - Régis Behmo - Julen Ruiz Aizpuru - astaric - Felix Yan - Philip_Tzou - Jesús Espino - Jeremy Weinstein - James Page - masklinn - Sjoerd Langkemper - Matt Iversen - Alexander A. Dyshev - Dirkjan Ochtman - Nick Retallack - Thomas Waldmann - xen Babel was previously developed under the Copyright of Edgewall Software. The following copyright notice holds true for releases before 2013: "Copyright (c) 2007 - 2011 by Edgewall Software" In addition to the regular contributions Babel includes a fork of Lennart Regebro's tzlocal that originally was licensed under the CC0 license. The original copyright of that project is "Copyright 2013 by Lennart Regebro". babel-2.14.0/babel/0000755000175000017500000000000014536056757013260 5ustar nileshnileshbabel-2.14.0/babel/localtime/0000755000175000017500000000000014536056757015231 5ustar nileshnileshbabel-2.14.0/babel/localtime/_unix.py0000644000175000017500000000657714536056757016744 0ustar nileshnileshimport datetime import os import re from babel.localtime._helpers import ( _get_tzinfo, _get_tzinfo_from_file, _get_tzinfo_or_raise, ) def _tz_from_env(tzenv: str) -> datetime.tzinfo: if tzenv[0] == ':': tzenv = tzenv[1:] # TZ specifies a file if os.path.exists(tzenv): return _get_tzinfo_from_file(tzenv) # TZ specifies a zoneinfo zone. return _get_tzinfo_or_raise(tzenv) def _get_localzone(_root: str = '/') -> datetime.tzinfo: """Tries to find the local timezone configuration. This method prefers finding the timezone name and passing that to zoneinfo or pytz, over passing in the localtime file, as in the later case the zoneinfo name is unknown. The parameter _root makes the function look for files like /etc/localtime beneath the _root directory. This is primarily used by the tests. In normal usage you call the function without parameters. """ tzenv = os.environ.get('TZ') if tzenv: return _tz_from_env(tzenv) # This is actually a pretty reliable way to test for the local time # zone on operating systems like OS X. On OS X especially this is the # only one that actually works. try: link_dst = os.readlink('/etc/localtime') except OSError: pass else: pos = link_dst.find('/zoneinfo/') if pos >= 0: zone_name = link_dst[pos + 10:] tzinfo = _get_tzinfo(zone_name) if tzinfo is not None: return tzinfo # Now look for distribution specific configuration files # that contain the timezone name. tzpath = os.path.join(_root, 'etc/timezone') if os.path.exists(tzpath): with open(tzpath, 'rb') as tzfile: data = tzfile.read() # Issue #3 in tzlocal was that /etc/timezone was a zoneinfo file. # That's a misconfiguration, but we need to handle it gracefully: if data[:5] != b'TZif2': etctz = data.strip().decode() # Get rid of host definitions and comments: if ' ' in etctz: etctz, dummy = etctz.split(' ', 1) if '#' in etctz: etctz, dummy = etctz.split('#', 1) return _get_tzinfo_or_raise(etctz.replace(' ', '_')) # CentOS has a ZONE setting in /etc/sysconfig/clock, # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and # Gentoo has a TIMEZONE setting in /etc/conf.d/clock # We look through these files for a timezone: timezone_re = re.compile(r'\s*(TIME)?ZONE\s*=\s*"(?P.+)"') for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'): tzpath = os.path.join(_root, filename) if not os.path.exists(tzpath): continue with open(tzpath) as tzfile: for line in tzfile: match = timezone_re.match(line) if match is not None: # We found a timezone etctz = match.group("etctz") return _get_tzinfo_or_raise(etctz.replace(' ', '_')) # No explicit setting existed. Use localtime for filename in ('etc/localtime', 'usr/local/etc/localtime'): tzpath = os.path.join(_root, filename) if not os.path.exists(tzpath): continue return _get_tzinfo_from_file(tzpath) raise LookupError('Can not find any timezone configuration') babel-2.14.0/babel/localtime/_helpers.py0000644000175000017500000000204414536056757017404 0ustar nileshnileshtry: import pytz except ModuleNotFoundError: pytz = None import zoneinfo def _get_tzinfo(tzenv: str): """Get the tzinfo from `zoneinfo` or `pytz` :param tzenv: timezone in the form of Continent/City :return: tzinfo object or None if not found """ if pytz: try: return pytz.timezone(tzenv) except pytz.UnknownTimeZoneError: pass else: try: return zoneinfo.ZoneInfo(tzenv) except zoneinfo.ZoneInfoNotFoundError: pass return None def _get_tzinfo_or_raise(tzenv: str): tzinfo = _get_tzinfo(tzenv) if tzinfo is None: raise LookupError( f"Can not find timezone {tzenv}. \n" "Timezone names are generally in the form `Continent/City`.", ) return tzinfo def _get_tzinfo_from_file(tzfilename: str): with open(tzfilename, 'rb') as tzfile: if pytz: return pytz.tzfile.build_tzinfo('local', tzfile) else: return zoneinfo.ZoneInfo.from_file(tzfile) babel-2.14.0/babel/localtime/_fallback.py0000644000175000017500000000226714536056757017510 0ustar nileshnilesh""" babel.localtime._fallback ~~~~~~~~~~~~~~~~~~~~~~~~~ Emulated fallback local timezone when all else fails. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ import datetime import time STDOFFSET = datetime.timedelta(seconds=-time.timezone) DSTOFFSET = datetime.timedelta(seconds=-time.altzone) if time.daylight else STDOFFSET DSTDIFF = DSTOFFSET - STDOFFSET ZERO = datetime.timedelta(0) class _FallbackLocalTimezone(datetime.tzinfo): def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta: if self._isdst(dt): return DSTOFFSET else: return STDOFFSET def dst(self, dt: datetime.datetime) -> datetime.timedelta: if self._isdst(dt): return DSTDIFF else: return ZERO def tzname(self, dt: datetime.datetime) -> str: return time.tzname[self._isdst(dt)] def _isdst(self, dt: datetime.datetime) -> bool: tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1) stamp = time.mktime(tt) tt = time.localtime(stamp) return tt.tm_isdst > 0 babel-2.14.0/babel/localtime/__init__.py0000644000175000017500000000202314536056757017337 0ustar nileshnilesh""" babel.localtime ~~~~~~~~~~~~~~~ Babel specific fork of tzlocal to determine the local timezone of the system. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ import datetime import sys if sys.platform == 'win32': from babel.localtime._win32 import _get_localzone else: from babel.localtime._unix import _get_localzone # TODO(3.0): the offset constants are not part of the public API # and should be removed from babel.localtime._fallback import ( DSTDIFF, # noqa: F401 DSTOFFSET, # noqa: F401 STDOFFSET, # noqa: F401 ZERO, # noqa: F401 _FallbackLocalTimezone, ) def get_localzone() -> datetime.tzinfo: """Returns the current underlying local timezone object. Generally this function does not need to be used, it's a better idea to use the :data:`LOCALTZ` singleton instead. """ return _get_localzone() try: LOCALTZ = get_localzone() except LookupError: LOCALTZ = _FallbackLocalTimezone() babel-2.14.0/babel/localtime/_win32.py0000644000175000017500000000621314536056757016706 0ustar nileshnileshfrom __future__ import annotations try: import winreg except ImportError: winreg = None import datetime from typing import Any, Dict, cast from babel.core import get_global from babel.localtime._helpers import _get_tzinfo_or_raise # When building the cldr data on windows this module gets imported. # Because at that point there is no global.dat yet this call will # fail. We want to catch it down in that case then and just assume # the mapping was empty. try: tz_names: dict[str, str] = cast(Dict[str, str], get_global('windows_zone_mapping')) except RuntimeError: tz_names = {} def valuestodict(key) -> dict[str, Any]: """Convert a registry key's values to a dictionary.""" dict = {} size = winreg.QueryInfoKey(key)[1] for i in range(size): data = winreg.EnumValue(key, i) dict[data[0]] = data[1] return dict def get_localzone_name() -> str: # Windows is special. It has unique time zone names (in several # meanings of the word) available, but unfortunately, they can be # translated to the language of the operating system, so we need to # do a backwards lookup, by going through all time zones and see which # one matches. handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) TZLOCALKEYNAME = r'SYSTEM\CurrentControlSet\Control\TimeZoneInformation' localtz = winreg.OpenKey(handle, TZLOCALKEYNAME) keyvalues = valuestodict(localtz) localtz.Close() if 'TimeZoneKeyName' in keyvalues: # Windows 7 (and Vista?) # For some reason this returns a string with loads of NUL bytes at # least on some systems. I don't know if this is a bug somewhere, I # just work around it. tzkeyname = keyvalues['TimeZoneKeyName'].split('\x00', 1)[0] else: # Windows 2000 or XP # This is the localized name: tzwin = keyvalues['StandardName'] # Open the list of timezones to look up the real name: TZKEYNAME = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones' tzkey = winreg.OpenKey(handle, TZKEYNAME) # Now, match this value to Time Zone information tzkeyname = None for i in range(winreg.QueryInfoKey(tzkey)[0]): subkey = winreg.EnumKey(tzkey, i) sub = winreg.OpenKey(tzkey, subkey) data = valuestodict(sub) sub.Close() if data.get('Std', None) == tzwin: tzkeyname = subkey break tzkey.Close() handle.Close() if tzkeyname is None: raise LookupError('Can not find Windows timezone configuration') timezone = tz_names.get(tzkeyname) if timezone is None: # Nope, that didn't work. Try adding 'Standard Time', # it seems to work a lot of times: timezone = tz_names.get(f"{tzkeyname} Standard Time") # Return what we have. if timezone is None: raise LookupError(f"Can not find timezone {tzkeyname}") return timezone def _get_localzone() -> datetime.tzinfo: if winreg is None: raise LookupError( 'Runtime support not available') return _get_tzinfo_or_raise(get_localzone_name()) babel-2.14.0/babel/units.py0000644000175000017500000003242614536056757015003 0ustar nileshnileshfrom __future__ import annotations import decimal from typing import TYPE_CHECKING from babel.core import Locale from babel.numbers import LC_NUMERIC, format_decimal if TYPE_CHECKING: from typing_extensions import Literal class UnknownUnitError(ValueError): def __init__(self, unit: str, locale: Locale) -> None: ValueError.__init__(self, f"{unit} is not a known unit in {locale}") def get_unit_name( measurement_unit: str, length: Literal['short', 'long', 'narrow'] = 'long', locale: Locale | str | None = LC_NUMERIC, ) -> str | None: """ Get the display name for a measurement unit in the given locale. >>> get_unit_name("radian", locale="en") 'radians' Unknown units will raise exceptions: >>> get_unit_name("battery", locale="fi") Traceback (most recent call last): ... UnknownUnitError: battery/long is not a known unit/length in fi :param measurement_unit: the code of a measurement unit. Known units can be found in the CLDR Unit Validity XML file: https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml :param length: "short", "long" or "narrow" :param locale: the `Locale` object or locale identifier :return: The unit display name, or None. """ locale = Locale.parse(locale) unit = _find_unit_pattern(measurement_unit, locale=locale) if not unit: raise UnknownUnitError(unit=measurement_unit, locale=locale) return locale.unit_display_names.get(unit, {}).get(length) def _find_unit_pattern(unit_id: str, locale: Locale | str | None = LC_NUMERIC) -> str | None: """ Expand a unit into a qualified form. Known units can be found in the CLDR Unit Validity XML file: https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml >>> _find_unit_pattern("radian", locale="en") 'angle-radian' Unknown values will return None. >>> _find_unit_pattern("horse", locale="en") :param unit_id: the code of a measurement unit. :return: A key to the `unit_patterns` mapping, or None. """ locale = Locale.parse(locale) unit_patterns = locale._data["unit_patterns"] if unit_id in unit_patterns: return unit_id for unit_pattern in sorted(unit_patterns, key=len): if unit_pattern.endswith(unit_id): return unit_pattern return None def format_unit( value: str | float | decimal.Decimal, measurement_unit: str, length: Literal['short', 'long', 'narrow'] = 'long', format: str | None = None, locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Format a value of a given unit. Values are formatted according to the locale's usual pluralization rules and number formats. >>> format_unit(12, 'length-meter', locale='ro_RO') u'12 metri' >>> format_unit(15.5, 'length-mile', locale='fi_FI') u'15,5 mailia' >>> format_unit(1200, 'pressure-millimeter-ofhg', locale='nb') u'1\\xa0200 millimeter kvikks\\xf8lv' >>> format_unit(270, 'ton', locale='en') u'270 tons' >>> format_unit(1234.5, 'kilogram', locale='ar_EG', numbering_system='default') u'1٬234٫5 كيلوغرام' Number formats may be overridden with the ``format`` parameter. >>> import decimal >>> format_unit(decimal.Decimal("-42.774"), 'temperature-celsius', 'short', format='#.0', locale='fr') u'-42,8\\u202f\\xb0C' The locale's usual pluralization rules are respected. >>> format_unit(1, 'length-meter', locale='ro_RO') u'1 metru' >>> format_unit(0, 'length-mile', locale='cy') u'0 mi' >>> format_unit(1, 'length-mile', locale='cy') u'1 filltir' >>> format_unit(3, 'length-mile', locale='cy') u'3 milltir' >>> format_unit(15, 'length-horse', locale='fi') Traceback (most recent call last): ... UnknownUnitError: length-horse is not a known unit in fi .. versionadded:: 2.2.0 :param value: the value to format. If this is a string, no number formatting will be attempted. :param measurement_unit: the code of a measurement unit. Known units can be found in the CLDR Unit Validity XML file: https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml :param length: "short", "long" or "narrow" :param format: An optional format, as accepted by `format_decimal`. :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) q_unit = _find_unit_pattern(measurement_unit, locale=locale) if not q_unit: raise UnknownUnitError(unit=measurement_unit, locale=locale) unit_patterns = locale._data["unit_patterns"][q_unit].get(length, {}) if isinstance(value, str): # Assume the value is a preformatted singular. formatted_value = value plural_form = "one" else: formatted_value = format_decimal(value, format, locale, numbering_system=numbering_system) plural_form = locale.plural_form(value) if plural_form in unit_patterns: return unit_patterns[plural_form].format(formatted_value) # Fall back to a somewhat bad representation. # nb: This is marked as no-cover, as the current CLDR seemingly has no way for this to happen. fallback_name = get_unit_name(measurement_unit, length=length, locale=locale) # pragma: no cover return f"{formatted_value} {fallback_name or measurement_unit}" # pragma: no cover def _find_compound_unit( numerator_unit: str, denominator_unit: str, locale: Locale | str | None = LC_NUMERIC, ) -> str | None: """ Find a predefined compound unit pattern. Used internally by format_compound_unit. >>> _find_compound_unit("kilometer", "hour", locale="en") 'speed-kilometer-per-hour' >>> _find_compound_unit("mile", "gallon", locale="en") 'consumption-mile-per-gallon' If no predefined compound pattern can be found, `None` is returned. >>> _find_compound_unit("gallon", "mile", locale="en") >>> _find_compound_unit("horse", "purple", locale="en") :param numerator_unit: The numerator unit's identifier :param denominator_unit: The denominator unit's identifier :param locale: the `Locale` object or locale identifier :return: A key to the `unit_patterns` mapping, or None. :rtype: str|None """ locale = Locale.parse(locale) # Qualify the numerator and denominator units. This will turn possibly partial # units like "kilometer" or "hour" into actual units like "length-kilometer" and # "duration-hour". resolved_numerator_unit = _find_unit_pattern(numerator_unit, locale=locale) resolved_denominator_unit = _find_unit_pattern(denominator_unit, locale=locale) # If either was not found, we can't possibly build a suitable compound unit either. if not (resolved_numerator_unit and resolved_denominator_unit): return None # Since compound units are named "speed-kilometer-per-hour", we'll have to slice off # the quantities (i.e. "length", "duration") from both qualified units. bare_numerator_unit = resolved_numerator_unit.split("-", 1)[-1] bare_denominator_unit = resolved_denominator_unit.split("-", 1)[-1] # Now we can try and rebuild a compound unit specifier, then qualify it: return _find_unit_pattern(f"{bare_numerator_unit}-per-{bare_denominator_unit}", locale=locale) def format_compound_unit( numerator_value: str | float | decimal.Decimal, numerator_unit: str | None = None, denominator_value: str | float | decimal.Decimal = 1, denominator_unit: str | None = None, length: Literal["short", "long", "narrow"] = "long", format: str | None = None, locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str | None: """ Format a compound number value, i.e. "kilometers per hour" or similar. Both unit specifiers are optional to allow for formatting of arbitrary values still according to the locale's general "per" formatting specifier. >>> format_compound_unit(7, denominator_value=11, length="short", locale="pt") '7/11' >>> format_compound_unit(150, "kilometer", denominator_unit="hour", locale="sv") '150 kilometer per timme' >>> format_compound_unit(150, "kilowatt", denominator_unit="year", locale="fi") '150 kilowattia / vuosi' >>> format_compound_unit(32.5, "ton", 15, denominator_unit="hour", locale="en") '32.5 tons per 15 hours' >>> format_compound_unit(1234.5, "ton", 15, denominator_unit="hour", locale="ar_EG", numbering_system="arab") '1٬234٫5 طن لكل 15 ساعة' >>> format_compound_unit(160, denominator_unit="square-meter", locale="fr") '160 par m\\xe8tre carr\\xe9' >>> format_compound_unit(4, "meter", "ratakisko", length="short", locale="fi") '4 m/ratakisko' >>> format_compound_unit(35, "minute", denominator_unit="fathom", locale="sv") '35 minuter per famn' >>> from babel.numbers import format_currency >>> format_compound_unit(format_currency(35, "JPY", locale="de"), denominator_unit="liter", locale="de") '35\\xa0\\xa5 pro Liter' See https://www.unicode.org/reports/tr35/tr35-general.html#perUnitPatterns :param numerator_value: The numerator value. This may be a string, in which case it is considered preformatted and the unit is ignored. :param numerator_unit: The numerator unit. See `format_unit`. :param denominator_value: The denominator value. This may be a string, in which case it is considered preformatted and the unit is ignored. :param denominator_unit: The denominator unit. See `format_unit`. :param length: The formatting length. "short", "long" or "narrow" :param format: An optional format, as accepted by `format_decimal`. :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :return: A formatted compound value. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) # Look for a specific compound unit first... if numerator_unit and denominator_unit and denominator_value == 1: compound_unit = _find_compound_unit(numerator_unit, denominator_unit, locale=locale) if compound_unit: return format_unit( numerator_value, compound_unit, length=length, format=format, locale=locale, numbering_system=numbering_system, ) # ... failing that, construct one "by hand". if isinstance(numerator_value, str): # Numerator is preformatted formatted_numerator = numerator_value elif numerator_unit: # Numerator has unit formatted_numerator = format_unit( numerator_value, numerator_unit, length=length, format=format, locale=locale, numbering_system=numbering_system, ) else: # Unitless numerator formatted_numerator = format_decimal( numerator_value, format=format, locale=locale, numbering_system=numbering_system, ) if isinstance(denominator_value, str): # Denominator is preformatted formatted_denominator = denominator_value elif denominator_unit: # Denominator has unit if denominator_value == 1: # support perUnitPatterns when the denominator is 1 denominator_unit = _find_unit_pattern(denominator_unit, locale=locale) per_pattern = locale._data["unit_patterns"].get(denominator_unit, {}).get(length, {}).get("per") if per_pattern: return per_pattern.format(formatted_numerator) # See TR-35's per-unit pattern algorithm, point 3.2. # For denominator 1, we replace the value to be formatted with the empty string; # this will make `format_unit` return " second" instead of "1 second". denominator_value = "" formatted_denominator = format_unit( denominator_value, measurement_unit=(denominator_unit or ""), length=length, format=format, locale=locale, numbering_system=numbering_system, ).strip() else: # Bare denominator formatted_denominator = format_decimal( denominator_value, format=format, locale=locale, numbering_system=numbering_system, ) # TODO: this doesn't support "compound_variations" (or "prefix"), and will fall back to the "x/y" representation per_pattern = locale._data["compound_unit_patterns"].get("per", {}).get(length, {}).get("compound", "{0}/{1}") return per_pattern.format(formatted_numerator, formatted_denominator) babel-2.14.0/babel/lists.py0000644000175000017500000000571014536056757014773 0ustar nileshnilesh""" babel.lists ~~~~~~~~~~~ Locale dependent formatting of lists. The default locale for the functions in this module is determined by the following environment variables, in that order: * ``LC_ALL``, and * ``LANG`` :copyright: (c) 2015-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations from collections.abc import Sequence from typing import TYPE_CHECKING from babel.core import Locale, default_locale if TYPE_CHECKING: from typing_extensions import Literal DEFAULT_LOCALE = default_locale() def format_list(lst: Sequence[str], style: Literal['standard', 'standard-short', 'or', 'or-short', 'unit', 'unit-short', 'unit-narrow'] = 'standard', locale: Locale | str | None = DEFAULT_LOCALE) -> str: """ Format the items in `lst` as a list. >>> format_list(['apples', 'oranges', 'pears'], locale='en') u'apples, oranges, and pears' >>> format_list(['apples', 'oranges', 'pears'], locale='zh') u'apples\u3001oranges\u548cpears' >>> format_list(['omena', 'peruna', 'aplari'], style='or', locale='fi') u'omena, peruna tai aplari' These styles are defined, but not all are necessarily available in all locales. The following text is verbatim from the Unicode TR35-49 spec [1]. * standard: A typical 'and' list for arbitrary placeholders. eg. "January, February, and March" * standard-short: A short version of an 'and' list, suitable for use with short or abbreviated placeholder values. eg. "Jan., Feb., and Mar." * or: A typical 'or' list for arbitrary placeholders. eg. "January, February, or March" * or-short: A short version of an 'or' list. eg. "Jan., Feb., or Mar." * unit: A list suitable for wide units. eg. "3 feet, 7 inches" * unit-short: A list suitable for short units eg. "3 ft, 7 in" * unit-narrow: A list suitable for narrow units, where space on the screen is very limited. eg. "3′ 7″" [1]: https://www.unicode.org/reports/tr35/tr35-49/tr35-general.html#ListPatterns :param lst: a sequence of items to format in to a list :param style: the style to format the list with. See above for description. :param locale: the locale """ locale = Locale.parse(locale) if not lst: return '' if len(lst) == 1: return lst[0] if style not in locale.list_patterns: raise ValueError( f'Locale {locale} does not support list formatting style {style!r} ' f'(supported are {sorted(locale.list_patterns)})', ) patterns = locale.list_patterns[style] if len(lst) == 2: return patterns['2'].format(*lst) result = patterns['start'].format(lst[0], lst[1]) for elem in lst[2:-1]: result = patterns['middle'].format(result, elem) result = patterns['end'].format(result, lst[-1]) return result babel-2.14.0/babel/locale-data/0000755000175000017500000000000014536056757015426 5ustar nileshnileshbabel-2.14.0/babel/locale-data/.gitignore0000644000175000017500000000000214536056757017406 0ustar nileshnilesh* babel-2.14.0/babel/dates.py0000644000175000017500000021602114536056757014734 0ustar nileshnilesh""" babel.dates ~~~~~~~~~~~ Locale dependent formatting and parsing of dates and times. The default locale for the functions in this module is determined by the following environment variables, in that order: * ``LC_TIME``, * ``LC_ALL``, and * ``LANG`` :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import re import warnings from functools import lru_cache from typing import TYPE_CHECKING, SupportsInt try: import pytz except ModuleNotFoundError: pytz = None import zoneinfo import datetime from collections.abc import Iterable from babel import localtime from babel.core import Locale, default_locale, get_global from babel.localedata import LocaleDataDict if TYPE_CHECKING: from typing_extensions import Literal, TypeAlias _Instant: TypeAlias = datetime.date | datetime.time | float | None _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short'] _Context: TypeAlias = Literal['format', 'stand-alone'] _DtOrTzinfo: TypeAlias = datetime.datetime | datetime.tzinfo | str | int | datetime.time | None # "If a given short metazone form is known NOT to be understood in a given # locale and the parent locale has this value such that it would normally # be inherited, the inheritance of this value can be explicitly disabled by # use of the 'no inheritance marker' as the value, which is 3 simultaneous [sic] # empty set characters ( U+2205 )." # - https://www.unicode.org/reports/tr35/tr35-dates.html#Metazone_Names NO_INHERITANCE_MARKER = '\u2205\u2205\u2205' UTC = datetime.timezone.utc LOCALTZ = localtime.LOCALTZ LC_TIME = default_locale('LC_TIME') def _localize(tz: datetime.tzinfo, dt: datetime.datetime) -> datetime.datetime: # Support localizing with both pytz and zoneinfo tzinfos # nothing to do if dt.tzinfo is tz: return dt if hasattr(tz, 'localize'): # pytz return tz.localize(dt) if dt.tzinfo is None: # convert naive to localized return dt.replace(tzinfo=tz) # convert timezones return dt.astimezone(tz) def _get_dt_and_tzinfo(dt_or_tzinfo: _DtOrTzinfo) -> tuple[datetime.datetime | None, datetime.tzinfo]: """ Parse a `dt_or_tzinfo` value into a datetime and a tzinfo. See the docs for this function's callers for semantics. :rtype: tuple[datetime, tzinfo] """ if dt_or_tzinfo is None: dt = datetime.datetime.now() tzinfo = LOCALTZ elif isinstance(dt_or_tzinfo, str): dt = None tzinfo = get_timezone(dt_or_tzinfo) elif isinstance(dt_or_tzinfo, int): dt = None tzinfo = UTC elif isinstance(dt_or_tzinfo, (datetime.datetime, datetime.time)): dt = _get_datetime(dt_or_tzinfo) tzinfo = dt.tzinfo if dt.tzinfo is not None else UTC else: dt = None tzinfo = dt_or_tzinfo return dt, tzinfo def _get_tz_name(dt_or_tzinfo: _DtOrTzinfo) -> str: """ Get the timezone name out of a time, datetime, or tzinfo object. :rtype: str """ dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) if hasattr(tzinfo, 'zone'): # pytz object return tzinfo.zone elif hasattr(tzinfo, 'key') and tzinfo.key is not None: # ZoneInfo object return tzinfo.key else: return tzinfo.tzname(dt or datetime.datetime.now(UTC)) def _get_datetime(instant: _Instant) -> datetime.datetime: """ Get a datetime out of an "instant" (date, time, datetime, number). .. warning:: The return values of this function may depend on the system clock. If the instant is None, the current moment is used. If the instant is a time, it's augmented with today's date. Dates are converted to naive datetimes with midnight as the time component. >>> from datetime import date, datetime >>> _get_datetime(date(2015, 1, 1)) datetime.datetime(2015, 1, 1, 0, 0) UNIX timestamps are converted to datetimes. >>> _get_datetime(1400000000) datetime.datetime(2014, 5, 13, 16, 53, 20) Other values are passed through as-is. >>> x = datetime(2015, 1, 1) >>> _get_datetime(x) is x True :param instant: date, time, datetime, integer, float or None :type instant: date|time|datetime|int|float|None :return: a datetime :rtype: datetime """ if instant is None: return datetime.datetime.now(UTC).replace(tzinfo=None) elif isinstance(instant, (int, float)): return datetime.datetime.fromtimestamp(instant, UTC).replace(tzinfo=None) elif isinstance(instant, datetime.time): return datetime.datetime.combine(datetime.date.today(), instant) elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): return datetime.datetime.combine(instant, datetime.time()) # TODO (3.x): Add an assertion/type check for this fallthrough branch: return instant def _ensure_datetime_tzinfo(dt: datetime.datetime, tzinfo: datetime.tzinfo | None = None) -> datetime.datetime: """ Ensure the datetime passed has an attached tzinfo. If the datetime is tz-naive to begin with, UTC is attached. If a tzinfo is passed in, the datetime is normalized to that timezone. >>> from datetime import datetime >>> _get_tz_name(_ensure_datetime_tzinfo(datetime(2015, 1, 1))) 'UTC' >>> tz = get_timezone("Europe/Stockholm") >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1, 13, 15, tzinfo=UTC), tzinfo=tz).hour 14 :param datetime: Datetime to augment. :param tzinfo: optional tzinfo :return: datetime with tzinfo :rtype: datetime """ if dt.tzinfo is None: dt = dt.replace(tzinfo=UTC) if tzinfo is not None: dt = dt.astimezone(get_timezone(tzinfo)) if hasattr(tzinfo, 'normalize'): # pytz dt = tzinfo.normalize(dt) return dt def _get_time( time: datetime.time | datetime.datetime | None, tzinfo: datetime.tzinfo | None = None, ) -> datetime.time: """ Get a timezoned time from a given instant. .. warning:: The return values of this function may depend on the system clock. :param time: time, datetime or None :rtype: time """ if time is None: time = datetime.datetime.now(UTC) elif isinstance(time, (int, float)): time = datetime.datetime.fromtimestamp(time, UTC) if time.tzinfo is None: time = time.replace(tzinfo=UTC) if isinstance(time, datetime.datetime): if tzinfo is not None: time = time.astimezone(tzinfo) if hasattr(tzinfo, 'normalize'): # pytz time = tzinfo.normalize(time) time = time.timetz() elif tzinfo is not None: time = time.replace(tzinfo=tzinfo) return time def get_timezone(zone: str | datetime.tzinfo | None = None) -> datetime.tzinfo: """Looks up a timezone by name and returns it. The timezone object returned comes from ``pytz`` or ``zoneinfo``, whichever is available. It corresponds to the `tzinfo` interface and can be used with all of the functions of Babel that operate with dates. If a timezone is not known a :exc:`LookupError` is raised. If `zone` is ``None`` a local zone object is returned. :param zone: the name of the timezone to look up. If a timezone object itself is passed in, it's returned unchanged. """ if zone is None: return LOCALTZ if not isinstance(zone, str): return zone if pytz: try: return pytz.timezone(zone) except pytz.UnknownTimeZoneError as e: exc = e else: assert zoneinfo try: return zoneinfo.ZoneInfo(zone) except zoneinfo.ZoneInfoNotFoundError as e: exc = e raise LookupError(f"Unknown timezone {zone}") from exc def get_period_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', context: _Context = 'stand-alone', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: """Return the names for day periods (AM/PM) used by the locale. >>> get_period_names(locale='en_US')['am'] u'AM' :param width: the width to use, one of "abbreviated", "narrow", or "wide" :param context: the context, either "format" or "stand-alone" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).day_periods[context][width] def get_day_names(width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide', context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: """Return the day names used by the locale for the specified format. >>> get_day_names('wide', locale='en_US')[1] u'Tuesday' >>> get_day_names('short', locale='en_US')[1] u'Tu' >>> get_day_names('abbreviated', locale='es')[1] u'mar' >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] u'D' :param width: the width to use, one of "wide", "abbreviated", "short" or "narrow" :param context: the context, either "format" or "stand-alone" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).days[context][width] def get_month_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: """Return the month names used by the locale for the specified format. >>> get_month_names('wide', locale='en_US')[1] u'January' >>> get_month_names('abbreviated', locale='es')[1] u'ene' >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1] u'J' :param width: the width to use, one of "wide", "abbreviated", or "narrow" :param context: the context, either "format" or "stand-alone" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).months[context][width] def get_quarter_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: """Return the quarter names used by the locale for the specified format. >>> get_quarter_names('wide', locale='en_US')[1] u'1st quarter' >>> get_quarter_names('abbreviated', locale='de_DE')[1] u'Q1' >>> get_quarter_names('narrow', locale='de_DE')[1] u'1' :param width: the width to use, one of "wide", "abbreviated", or "narrow" :param context: the context, either "format" or "stand-alone" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).quarters[context][width] def get_era_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: """Return the era names used by the locale for the specified format. >>> get_era_names('wide', locale='en_US')[1] u'Anno Domini' >>> get_era_names('abbreviated', locale='de_DE')[1] u'n. Chr.' :param width: the width to use, either "wide", "abbreviated", or "narrow" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).eras[width] def get_date_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern: """Return the date formatting patterns used by the locale for the specified format. >>> get_date_format(locale='en_US') >>> get_date_format('full', locale='de_DE') :param format: the format to use, one of "full", "long", "medium", or "short" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).date_formats[format] def get_datetime_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern: """Return the datetime formatting patterns used by the locale for the specified format. >>> get_datetime_format(locale='en_US') u'{1}, {0}' :param format: the format to use, one of "full", "long", "medium", or "short" :param locale: the `Locale` object, or a locale string """ patterns = Locale.parse(locale).datetime_formats if format not in patterns: format = None return patterns[format] def get_time_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern: """Return the time formatting patterns used by the locale for the specified format. >>> get_time_format(locale='en_US') >>> get_time_format('full', locale='de_DE') :param format: the format to use, one of "full", "long", "medium", or "short" :param locale: the `Locale` object, or a locale string """ return Locale.parse(locale).time_formats[format] def get_timezone_gmt( datetime: _Instant = None, width: Literal['long', 'short', 'iso8601', 'iso8601_short'] = 'long', locale: Locale | str | None = LC_TIME, return_z: bool = False, ) -> str: """Return the timezone associated with the given `datetime` object formatted as string indicating the offset from GMT. >>> from datetime import datetime >>> dt = datetime(2007, 4, 1, 15, 30) >>> get_timezone_gmt(dt, locale='en') u'GMT+00:00' >>> get_timezone_gmt(dt, locale='en', return_z=True) 'Z' >>> get_timezone_gmt(dt, locale='en', width='iso8601_short') u'+00' >>> tz = get_timezone('America/Los_Angeles') >>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30)) >>> get_timezone_gmt(dt, locale='en') u'GMT-07:00' >>> get_timezone_gmt(dt, 'short', locale='en') u'-0700' >>> get_timezone_gmt(dt, locale='en', width='iso8601_short') u'-07' The long format depends on the locale, for example in France the acronym UTC string is used instead of GMT: >>> get_timezone_gmt(dt, 'long', locale='fr_FR') u'UTC-07:00' .. versionadded:: 0.9 :param datetime: the ``datetime`` object; if `None`, the current date and time in UTC is used :param width: either "long" or "short" or "iso8601" or "iso8601_short" :param locale: the `Locale` object, or a locale string :param return_z: True or False; Function returns indicator "Z" when local time offset is 0 """ datetime = _ensure_datetime_tzinfo(_get_datetime(datetime)) locale = Locale.parse(locale) offset = datetime.tzinfo.utcoffset(datetime) seconds = offset.days * 24 * 60 * 60 + offset.seconds hours, seconds = divmod(seconds, 3600) if return_z and hours == 0 and seconds == 0: return 'Z' elif seconds == 0 and width == 'iso8601_short': return '%+03d' % hours elif width == 'short' or width == 'iso8601_short': pattern = '%+03d%02d' elif width == 'iso8601': pattern = '%+03d:%02d' else: pattern = locale.zone_formats['gmt'] % '%+03d:%02d' return pattern % (hours, seconds // 60) def get_timezone_location( dt_or_tzinfo: _DtOrTzinfo = None, locale: Locale | str | None = LC_TIME, return_city: bool = False, ) -> str: """Return a representation of the given timezone using "location format". The result depends on both the local display name of the country and the city associated with the time zone: >>> tz = get_timezone('America/St_Johns') >>> print(get_timezone_location(tz, locale='de_DE')) Kanada (St. John’s) (Ortszeit) >>> print(get_timezone_location(tz, locale='en')) Canada (St. John’s) Time >>> print(get_timezone_location(tz, locale='en', return_city=True)) St. John’s >>> tz = get_timezone('America/Mexico_City') >>> get_timezone_location(tz, locale='de_DE') u'Mexiko (Mexiko-Stadt) (Ortszeit)' If the timezone is associated with a country that uses only a single timezone, just the localized country name is returned: >>> tz = get_timezone('Europe/Berlin') >>> get_timezone_name(tz, locale='de_DE') u'Mitteleurop\\xe4ische Zeit' .. versionadded:: 0.9 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines the timezone; if `None`, the current date and time in UTC is assumed :param locale: the `Locale` object, or a locale string :param return_city: True or False, if True then return exemplar city (location) for the time zone :return: the localized timezone name using location format """ locale = Locale.parse(locale) zone = _get_tz_name(dt_or_tzinfo) # Get the canonical time-zone code zone = get_global('zone_aliases').get(zone, zone) info = locale.time_zones.get(zone, {}) # Otherwise, if there is only one timezone for the country, return the # localized country name region_format = locale.zone_formats['region'] territory = get_global('zone_territories').get(zone) if territory not in locale.territories: territory = 'ZZ' # invalid/unknown territory_name = locale.territories[territory] if not return_city and territory and len(get_global('territory_zones').get(territory, [])) == 1: return region_format % territory_name # Otherwise, include the city in the output fallback_format = locale.zone_formats['fallback'] if 'city' in info: city_name = info['city'] else: metazone = get_global('meta_zones').get(zone) metazone_info = locale.meta_zones.get(metazone, {}) if 'city' in metazone_info: city_name = metazone_info['city'] elif '/' in zone: city_name = zone.split('/', 1)[1].replace('_', ' ') else: city_name = zone.replace('_', ' ') if return_city: return city_name return region_format % (fallback_format % { '0': city_name, '1': territory_name, }) def get_timezone_name( dt_or_tzinfo: _DtOrTzinfo = None, width: Literal['long', 'short'] = 'long', uncommon: bool = False, locale: Locale | str | None = LC_TIME, zone_variant: Literal['generic', 'daylight', 'standard'] | None = None, return_zone: bool = False, ) -> str: r"""Return the localized display name for the given timezone. The timezone may be specified using a ``datetime`` or `tzinfo` object. >>> from datetime import time >>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles')) >>> get_timezone_name(dt, locale='en_US') # doctest: +SKIP u'Pacific Standard Time' >>> get_timezone_name(dt, locale='en_US', return_zone=True) 'America/Los_Angeles' >>> get_timezone_name(dt, width='short', locale='en_US') # doctest: +SKIP u'PST' If this function gets passed only a `tzinfo` object and no concrete `datetime`, the returned display name is independent of daylight savings time. This can be used for example for selecting timezones, or to set the time of events that recur across DST changes: >>> tz = get_timezone('America/Los_Angeles') >>> get_timezone_name(tz, locale='en_US') u'Pacific Time' >>> get_timezone_name(tz, 'short', locale='en_US') u'PT' If no localized display name for the timezone is available, and the timezone is associated with a country that uses only a single timezone, the name of that country is returned, formatted according to the locale: >>> tz = get_timezone('Europe/Berlin') >>> get_timezone_name(tz, locale='de_DE') u'Mitteleurop\xe4ische Zeit' >>> get_timezone_name(tz, locale='pt_BR') u'Hor\xe1rio da Europa Central' On the other hand, if the country uses multiple timezones, the city is also included in the representation: >>> tz = get_timezone('America/St_Johns') >>> get_timezone_name(tz, locale='de_DE') u'Neufundland-Zeit' Note that short format is currently not supported for all timezones and all locales. This is partially because not every timezone has a short code in every locale. In that case it currently falls back to the long format. For more information see `LDML Appendix J: Time Zone Display Names `_ .. versionadded:: 0.9 .. versionchanged:: 1.0 Added `zone_variant` support. :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines the timezone; if a ``tzinfo`` object is used, the resulting display name will be generic, i.e. independent of daylight savings time; if `None`, the current date in UTC is assumed :param width: either "long" or "short" :param uncommon: deprecated and ignored :param zone_variant: defines the zone variation to return. By default the variation is defined from the datetime object passed in. If no datetime object is passed in, the ``'generic'`` variation is assumed. The following values are valid: ``'generic'``, ``'daylight'`` and ``'standard'``. :param locale: the `Locale` object, or a locale string :param return_zone: True or False. If true then function returns long time zone ID """ dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) locale = Locale.parse(locale) zone = _get_tz_name(dt_or_tzinfo) if zone_variant is None: if dt is None: zone_variant = 'generic' else: dst = tzinfo.dst(dt) zone_variant = "daylight" if dst else "standard" else: if zone_variant not in ('generic', 'standard', 'daylight'): raise ValueError('Invalid zone variation') # Get the canonical time-zone code zone = get_global('zone_aliases').get(zone, zone) if return_zone: return zone info = locale.time_zones.get(zone, {}) # Try explicitly translated zone names first if width in info and zone_variant in info[width]: return info[width][zone_variant] metazone = get_global('meta_zones').get(zone) if metazone: metazone_info = locale.meta_zones.get(metazone, {}) if width in metazone_info: name = metazone_info[width].get(zone_variant) if width == 'short' and name == NO_INHERITANCE_MARKER: # If the short form is marked no-inheritance, # try to fall back to the long name instead. name = metazone_info.get('long', {}).get(zone_variant) if name: return name # If we have a concrete datetime, we assume that the result can't be # independent of daylight savings time, so we return the GMT offset if dt is not None: return get_timezone_gmt(dt, width=width, locale=locale) return get_timezone_location(dt_or_tzinfo, locale=locale) def format_date( date: datetime.date | None = None, format: _PredefinedTimeFormat | str = 'medium', locale: Locale | str | None = LC_TIME, ) -> str: """Return a date formatted according to the given pattern. >>> from datetime import date >>> d = date(2007, 4, 1) >>> format_date(d, locale='en_US') u'Apr 1, 2007' >>> format_date(d, format='full', locale='de_DE') u'Sonntag, 1. April 2007' If you don't want to use the locale default formats, you can specify a custom date pattern: >>> format_date(d, "EEE, MMM d, ''yy", locale='en') u"Sun, Apr 1, '07" :param date: the ``date`` or ``datetime`` object; if `None`, the current date is used :param format: one of "full", "long", "medium", or "short", or a custom date/time pattern :param locale: a `Locale` object or a locale identifier """ if date is None: date = datetime.date.today() elif isinstance(date, datetime.datetime): date = date.date() locale = Locale.parse(locale) if format in ('full', 'long', 'medium', 'short'): format = get_date_format(format, locale=locale) pattern = parse_pattern(format) return pattern.apply(date, locale) def format_datetime( datetime: _Instant = None, format: _PredefinedTimeFormat | str = 'medium', tzinfo: datetime.tzinfo | None = None, locale: Locale | str | None = LC_TIME, ) -> str: r"""Return a date formatted according to the given pattern. >>> from datetime import datetime >>> dt = datetime(2007, 4, 1, 15, 30) >>> format_datetime(dt, locale='en_US') u'Apr 1, 2007, 3:30:00\u202fPM' For any pattern requiring the display of the timezone: >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'), ... locale='fr_FR') 'dimanche 1 avril 2007, 17:30:00 heure d’été d’Europe centrale' >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", ... tzinfo=get_timezone('US/Eastern'), locale='en') u'2007.04.01 AD at 11:30:00 EDT' :param datetime: the `datetime` object; if `None`, the current date and time is used :param format: one of "full", "long", "medium", or "short", or a custom date/time pattern :param tzinfo: the timezone to apply to the time for display :param locale: a `Locale` object or a locale identifier """ datetime = _ensure_datetime_tzinfo(_get_datetime(datetime), tzinfo) locale = Locale.parse(locale) if format in ('full', 'long', 'medium', 'short'): return get_datetime_format(format, locale=locale) \ .replace("'", "") \ .replace('{0}', format_time(datetime, format, tzinfo=None, locale=locale)) \ .replace('{1}', format_date(datetime, format, locale=locale)) else: return parse_pattern(format).apply(datetime, locale) def format_time( time: datetime.time | datetime.datetime | float | None = None, format: _PredefinedTimeFormat | str = 'medium', tzinfo: datetime.tzinfo | None = None, locale: Locale | str | None = LC_TIME, ) -> str: r"""Return a time formatted according to the given pattern. >>> from datetime import datetime, time >>> t = time(15, 30) >>> format_time(t, locale='en_US') u'3:30:00\u202fPM' >>> format_time(t, format='short', locale='de_DE') u'15:30' If you don't want to use the locale default formats, you can specify a custom time pattern: >>> format_time(t, "hh 'o''clock' a", locale='en') u"03 o'clock PM" For any pattern requiring the display of the time-zone a timezone has to be specified explicitly: >>> t = datetime(2007, 4, 1, 15, 30) >>> tzinfo = get_timezone('Europe/Paris') >>> t = _localize(tzinfo, t) >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR') '15:30:00 heure d’été d’Europe centrale' >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'), ... locale='en') u"09 o'clock AM, Eastern Daylight Time" As that example shows, when this function gets passed a ``datetime.datetime`` value, the actual time in the formatted string is adjusted to the timezone specified by the `tzinfo` parameter. If the ``datetime`` is "naive" (i.e. it has no associated timezone information), it is assumed to be in UTC. These timezone calculations are **not** performed if the value is of type ``datetime.time``, as without date information there's no way to determine what a given time would translate to in a different timezone without information about whether daylight savings time is in effect or not. This means that time values are left as-is, and the value of the `tzinfo` parameter is only used to display the timezone name if needed: >>> t = time(15, 30) >>> format_time(t, format='full', tzinfo=get_timezone('Europe/Paris'), ... locale='fr_FR') # doctest: +SKIP u'15:30:00 heure normale d\u2019Europe centrale' >>> format_time(t, format='full', tzinfo=get_timezone('US/Eastern'), ... locale='en_US') # doctest: +SKIP u'3:30:00\u202fPM Eastern Standard Time' :param time: the ``time`` or ``datetime`` object; if `None`, the current time in UTC is used :param format: one of "full", "long", "medium", or "short", or a custom date/time pattern :param tzinfo: the time-zone to apply to the time for display :param locale: a `Locale` object or a locale identifier """ # get reference date for if we need to find the right timezone variant # in the pattern ref_date = time.date() if isinstance(time, datetime.datetime) else None time = _get_time(time, tzinfo) locale = Locale.parse(locale) if format in ('full', 'long', 'medium', 'short'): format = get_time_format(format, locale=locale) return parse_pattern(format).apply(time, locale, reference_date=ref_date) def format_skeleton( skeleton: str, datetime: _Instant = None, tzinfo: datetime.tzinfo | None = None, fuzzy: bool = True, locale: Locale | str | None = LC_TIME, ) -> str: r"""Return a time and/or date formatted according to the given pattern. The skeletons are defined in the CLDR data and provide more flexibility than the simple short/long/medium formats, but are a bit harder to use. The are defined using the date/time symbols without order or punctuation and map to a suitable format for the given locale. >>> from datetime import datetime >>> t = datetime(2007, 4, 1, 15, 30) >>> format_skeleton('MMMEd', t, locale='fr') u'dim. 1 avr.' >>> format_skeleton('MMMEd', t, locale='en') u'Sun, Apr 1' >>> format_skeleton('yMMd', t, locale='fi') # yMMd is not in the Finnish locale; yMd gets used u'1.4.2007' >>> format_skeleton('yMMd', t, fuzzy=False, locale='fi') # yMMd is not in the Finnish locale, an error is thrown Traceback (most recent call last): ... KeyError: yMMd After the skeleton is resolved to a pattern `format_datetime` is called so all timezone processing etc is the same as for that. :param skeleton: A date time skeleton as defined in the cldr data. :param datetime: the ``time`` or ``datetime`` object; if `None`, the current time in UTC is used :param tzinfo: the time-zone to apply to the time for display :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's close enough to it. :param locale: a `Locale` object or a locale identifier """ locale = Locale.parse(locale) if fuzzy and skeleton not in locale.datetime_skeletons: skeleton = match_skeleton(skeleton, locale.datetime_skeletons) format = locale.datetime_skeletons[skeleton] return format_datetime(datetime, format, tzinfo, locale) TIMEDELTA_UNITS: tuple[tuple[str, int], ...] = ( ('year', 3600 * 24 * 365), ('month', 3600 * 24 * 30), ('week', 3600 * 24 * 7), ('day', 3600 * 24), ('hour', 3600), ('minute', 60), ('second', 1), ) def format_timedelta( delta: datetime.timedelta | int, granularity: Literal['year', 'month', 'week', 'day', 'hour', 'minute', 'second'] = 'second', threshold: float = .85, add_direction: bool = False, format: Literal['narrow', 'short', 'medium', 'long'] = 'long', locale: Locale | str | None = LC_TIME, ) -> str: """Return a time delta according to the rules of the given locale. >>> from datetime import timedelta >>> format_timedelta(timedelta(weeks=12), locale='en_US') u'3 months' >>> format_timedelta(timedelta(seconds=1), locale='es') u'1 segundo' The granularity parameter can be provided to alter the lowest unit presented, which defaults to a second. >>> format_timedelta(timedelta(hours=3), granularity='day', locale='en_US') u'1 day' The threshold parameter can be used to determine at which value the presentation switches to the next higher unit. A higher threshold factor means the presentation will switch later. For example: >>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US') u'1 day' >>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US') u'23 hours' In addition directional information can be provided that informs the user if the date is in the past or in the future: >>> format_timedelta(timedelta(hours=1), add_direction=True, locale='en') u'in 1 hour' >>> format_timedelta(timedelta(hours=-1), add_direction=True, locale='en') u'1 hour ago' The format parameter controls how compact or wide the presentation is: >>> format_timedelta(timedelta(hours=3), format='short', locale='en') u'3 hr' >>> format_timedelta(timedelta(hours=3), format='narrow', locale='en') u'3h' :param delta: a ``timedelta`` object representing the time difference to format, or the delta in seconds as an `int` value :param granularity: determines the smallest unit that should be displayed, the value can be one of "year", "month", "week", "day", "hour", "minute" or "second" :param threshold: factor that determines at which point the presentation switches to the next higher unit :param add_direction: if this flag is set to `True` the return value will include directional information. For instance a positive timedelta will include the information about it being in the future, a negative will be information about the value being in the past. :param format: the format, can be "narrow", "short" or "long". ( "medium" is deprecated, currently converted to "long" to maintain compatibility) :param locale: a `Locale` object or a locale identifier """ if format not in ('narrow', 'short', 'medium', 'long'): raise TypeError('Format must be one of "narrow", "short" or "long"') if format == 'medium': warnings.warn( '"medium" value for format param of format_timedelta' ' is deprecated. Use "long" instead', category=DeprecationWarning, stacklevel=2, ) format = 'long' if isinstance(delta, datetime.timedelta): seconds = int((delta.days * 86400) + delta.seconds) else: seconds = delta locale = Locale.parse(locale) def _iter_patterns(a_unit): if add_direction: unit_rel_patterns = locale._data['date_fields'][a_unit] if seconds >= 0: yield unit_rel_patterns['future'] else: yield unit_rel_patterns['past'] a_unit = f"duration-{a_unit}" yield locale._data['unit_patterns'].get(a_unit, {}).get(format) for unit, secs_per_unit in TIMEDELTA_UNITS: value = abs(seconds) / secs_per_unit if value >= threshold or unit == granularity: if unit == granularity and value > 0: value = max(1, value) value = int(round(value)) plural_form = locale.plural_form(value) pattern = None for patterns in _iter_patterns(unit): if patterns is not None: pattern = patterns.get(plural_form) or patterns.get('other') break # This really should not happen if pattern is None: return '' return pattern.replace('{0}', str(value)) return '' def _format_fallback_interval( start: _Instant, end: _Instant, skeleton: str | None, tzinfo: datetime.tzinfo | None, locale: Locale | str | None = LC_TIME, ) -> str: if skeleton in locale.datetime_skeletons: # Use the given skeleton format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale) elif all((isinstance(d, datetime.date) and not isinstance(d, datetime.datetime)) for d in (start, end)): # Both are just dates format = lambda dt: format_date(dt, locale=locale) elif all((isinstance(d, datetime.time) and not isinstance(d, datetime.date)) for d in (start, end)): # Both are times format = lambda dt: format_time(dt, tzinfo=tzinfo, locale=locale) else: format = lambda dt: format_datetime(dt, tzinfo=tzinfo, locale=locale) formatted_start = format(start) formatted_end = format(end) if formatted_start == formatted_end: return format(start) return ( locale.interval_formats.get(None, "{0}-{1}"). replace("{0}", formatted_start). replace("{1}", formatted_end) ) def format_interval( start: _Instant, end: _Instant, skeleton: str | None = None, tzinfo: datetime.tzinfo | None = None, fuzzy: bool = True, locale: Locale | str | None = LC_TIME, ) -> str: """ Format an interval between two instants according to the locale's rules. >>> from datetime import date, time >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "yMd", locale="fi") u'15.\u201317.1.2016' >>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB") '12:12\u201316:16' >>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US") '5:12\u202fAM\u2009–\u20094:16\u202fPM' >>> format_interval(time(16, 18), time(16, 24), "Hm", locale="it") '16:18\u201316:24' If the start instant equals the end instant, the interval is formatted like the instant. >>> format_interval(time(16, 18), time(16, 18), "Hm", locale="it") '16:18' Unknown skeletons fall back to "default" formatting. >>> format_interval(date(2015, 1, 1), date(2017, 1, 1), "wzq", locale="ja") '2015/01/01\uff5e2017/01/01' >>> format_interval(time(16, 18), time(16, 24), "xxx", locale="ja") '16:18:00\uff5e16:24:00' >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "xxx", locale="de") '15.01.2016\u2009–\u200917.01.2016' :param start: First instant (datetime/date/time) :param end: Second instant (datetime/date/time) :param skeleton: The "skeleton format" to use for formatting. :param tzinfo: tzinfo to use (if none is already attached) :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's close enough to it. :param locale: A locale object or identifier. :return: Formatted interval """ locale = Locale.parse(locale) # NB: The quote comments below are from the algorithm description in # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats # > Look for the intervalFormatItem element that matches the "skeleton", # > starting in the current locale and then following the locale fallback # > chain up to, but not including root. interval_formats = locale.interval_formats if skeleton not in interval_formats or not skeleton: # > If no match was found from the previous step, check what the closest # > match is in the fallback locale chain, as in availableFormats. That # > is, this allows for adjusting the string value field's width, # > including adjusting between "MMM" and "MMMM", and using different # > variants of the same field, such as 'v' and 'z'. if skeleton and fuzzy: skeleton = match_skeleton(skeleton, interval_formats) else: skeleton = None if not skeleton: # Still no match whatsoever? # > Otherwise, format the start and end datetime using the fallback pattern. return _format_fallback_interval(start, end, skeleton, tzinfo, locale) skel_formats = interval_formats[skeleton] if start == end: return format_skeleton(skeleton, start, tzinfo, fuzzy=fuzzy, locale=locale) start = _ensure_datetime_tzinfo(_get_datetime(start), tzinfo=tzinfo) end = _ensure_datetime_tzinfo(_get_datetime(end), tzinfo=tzinfo) start_fmt = DateTimeFormat(start, locale=locale) end_fmt = DateTimeFormat(end, locale=locale) # > If a match is found from previous steps, compute the calendar field # > with the greatest difference between start and end datetime. If there # > is no difference among any of the fields in the pattern, format as a # > single date using availableFormats, and return. for field in PATTERN_CHAR_ORDER: # These are in largest-to-smallest order if field in skel_formats and start_fmt.extract(field) != end_fmt.extract(field): # > If there is a match, use the pieces of the corresponding pattern to # > format the start and end datetime, as above. return "".join( parse_pattern(pattern).apply(instant, locale) for pattern, instant in zip(skel_formats[field], (start, end)) ) # > Otherwise, format the start and end datetime using the fallback pattern. return _format_fallback_interval(start, end, skeleton, tzinfo, locale) def get_period_id( time: _Instant, tzinfo: datetime.tzinfo | None = None, type: Literal['selection'] | None = None, locale: Locale | str | None = LC_TIME, ) -> str: """ Get the day period ID for a given time. This ID can be used as a key for the period name dictionary. >>> from datetime import time >>> get_period_names(locale="de")[get_period_id(time(7, 42), locale="de")] u'Morgen' >>> get_period_id(time(0), locale="en_US") u'midnight' >>> get_period_id(time(0), type="selection", locale="en_US") u'night1' :param time: The time to inspect. :param tzinfo: The timezone for the time. See ``format_time``. :param type: The period type to use. Either "selection" or None. The selection type is used for selecting among phrases such as “Your email arrived yesterday evening” or “Your email arrived last night”. :param locale: the `Locale` object, or a locale string :return: period ID. Something is always returned -- even if it's just "am" or "pm". """ time = _get_time(time, tzinfo) seconds_past_midnight = int(time.hour * 60 * 60 + time.minute * 60 + time.second) locale = Locale.parse(locale) # The LDML rules state that the rules may not overlap, so iterating in arbitrary # order should be alright, though `at` periods should be preferred. rulesets = locale.day_period_rules.get(type, {}).items() for rule_id, rules in rulesets: for rule in rules: if "at" in rule and rule["at"] == seconds_past_midnight: return rule_id for rule_id, rules in rulesets: for rule in rules: if "from" in rule and "before" in rule: if rule["from"] < rule["before"]: if rule["from"] <= seconds_past_midnight < rule["before"]: return rule_id else: # e.g. from="21:00" before="06:00" if rule["from"] <= seconds_past_midnight < 86400 or \ 0 <= seconds_past_midnight < rule["before"]: return rule_id start_ok = end_ok = False if "from" in rule and seconds_past_midnight >= rule["from"]: start_ok = True if "to" in rule and seconds_past_midnight <= rule["to"]: # This rule type does not exist in the present CLDR data; # excuse the lack of test coverage. end_ok = True if "before" in rule and seconds_past_midnight < rule["before"]: end_ok = True if "after" in rule: raise NotImplementedError("'after' is deprecated as of CLDR 29.") if start_ok and end_ok: return rule_id if seconds_past_midnight < 43200: return "am" else: return "pm" class ParseError(ValueError): pass def parse_date( string: str, locale: Locale | str | None = LC_TIME, format: _PredefinedTimeFormat = 'medium', ) -> datetime.date: """Parse a date from a string. This function first tries to interpret the string as ISO-8601 date format, then uses the date format for the locale as a hint to determine the order in which the date fields appear in the string. >>> parse_date('4/1/04', locale='en_US') datetime.date(2004, 4, 1) >>> parse_date('01.04.2004', locale='de_DE') datetime.date(2004, 4, 1) >>> parse_date('2004-04-01', locale='en_US') datetime.date(2004, 4, 1) >>> parse_date('2004-04-01', locale='de_DE') datetime.date(2004, 4, 1) :param string: the string containing the date :param locale: a `Locale` object or a locale identifier :param format: the format to use (see ``get_date_format``) """ numbers = re.findall(r'(\d+)', string) if not numbers: raise ParseError("No numbers were found in input") # we try ISO-8601 format first, meaning similar to formats # extended YYYY-MM-DD or basic YYYYMMDD iso_alike = re.match(r'^(\d{4})-?([01]\d)-?([0-3]\d)$', string, flags=re.ASCII) # allow only ASCII digits if iso_alike: try: return datetime.date(*map(int, iso_alike.groups())) except ValueError: pass # a locale format might fit better, so let's continue format_str = get_date_format(format=format, locale=locale).pattern.lower() year_idx = format_str.index('y') month_idx = format_str.index('m') if month_idx < 0: month_idx = format_str.index('l') day_idx = format_str.index('d') indexes = sorted([(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')]) indexes = {item[1]: idx for idx, item in enumerate(indexes)} # FIXME: this currently only supports numbers, but should also support month # names, both in the requested locale, and english year = numbers[indexes['Y']] year = 2000 + int(year) if len(year) == 2 else int(year) month = int(numbers[indexes['M']]) day = int(numbers[indexes['D']]) if month > 12: month, day = day, month return datetime.date(year, month, day) def parse_time( string: str, locale: Locale | str | None = LC_TIME, format: _PredefinedTimeFormat = 'medium', ) -> datetime.time: """Parse a time from a string. This function uses the time format for the locale as a hint to determine the order in which the time fields appear in the string. >>> parse_time('15:30:00', locale='en_US') datetime.time(15, 30) :param string: the string containing the time :param locale: a `Locale` object or a locale identifier :param format: the format to use (see ``get_time_format``) :return: the parsed time :rtype: `time` """ numbers = re.findall(r'(\d+)', string) if not numbers: raise ParseError("No numbers were found in input") # TODO: try ISO format first? format_str = get_time_format(format=format, locale=locale).pattern.lower() hour_idx = format_str.index('h') if hour_idx < 0: hour_idx = format_str.index('k') min_idx = format_str.index('m') sec_idx = format_str.index('s') indexes = sorted([(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')]) indexes = {item[1]: idx for idx, item in enumerate(indexes)} # TODO: support time zones # Check if the format specifies a period to be used; # if it does, look for 'pm' to figure out an offset. hour_offset = 0 if 'a' in format_str and 'pm' in string.lower(): hour_offset = 12 # Parse up to three numbers from the string. minute = second = 0 hour = int(numbers[indexes['H']]) + hour_offset if len(numbers) > 1: minute = int(numbers[indexes['M']]) if len(numbers) > 2: second = int(numbers[indexes['S']]) return datetime.time(hour, minute, second) class DateTimePattern: def __init__(self, pattern: str, format: DateTimeFormat): self.pattern = pattern self.format = format def __repr__(self) -> str: return f"<{type(self).__name__} {self.pattern!r}>" def __str__(self) -> str: pat = self.pattern return pat def __mod__(self, other: DateTimeFormat) -> str: if not isinstance(other, DateTimeFormat): return NotImplemented return self.format % other def apply( self, datetime: datetime.date | datetime.time, locale: Locale | str | None, reference_date: datetime.date | None = None, ) -> str: return self % DateTimeFormat(datetime, locale, reference_date) class DateTimeFormat: def __init__( self, value: datetime.date | datetime.time, locale: Locale | str, reference_date: datetime.date | None = None, ) -> None: assert isinstance(value, (datetime.date, datetime.datetime, datetime.time)) if isinstance(value, (datetime.datetime, datetime.time)) and value.tzinfo is None: value = value.replace(tzinfo=UTC) self.value = value self.locale = Locale.parse(locale) self.reference_date = reference_date def __getitem__(self, name: str) -> str: char = name[0] num = len(name) if char == 'G': return self.format_era(char, num) elif char in ('y', 'Y', 'u'): return self.format_year(char, num) elif char in ('Q', 'q'): return self.format_quarter(char, num) elif char in ('M', 'L'): return self.format_month(char, num) elif char in ('w', 'W'): return self.format_week(char, num) elif char == 'd': return self.format(self.value.day, num) elif char == 'D': return self.format_day_of_year(num) elif char == 'F': return self.format_day_of_week_in_month() elif char in ('E', 'e', 'c'): return self.format_weekday(char, num) elif char in ('a', 'b', 'B'): return self.format_period(char, num) elif char == 'h': if self.value.hour % 12 == 0: return self.format(12, num) else: return self.format(self.value.hour % 12, num) elif char == 'H': return self.format(self.value.hour, num) elif char == 'K': return self.format(self.value.hour % 12, num) elif char == 'k': if self.value.hour == 0: return self.format(24, num) else: return self.format(self.value.hour, num) elif char == 'm': return self.format(self.value.minute, num) elif char == 's': return self.format(self.value.second, num) elif char == 'S': return self.format_frac_seconds(num) elif char == 'A': return self.format_milliseconds_in_day(num) elif char in ('z', 'Z', 'v', 'V', 'x', 'X', 'O'): return self.format_timezone(char, num) else: raise KeyError(f"Unsupported date/time field {char!r}") def extract(self, char: str) -> int: char = str(char)[0] if char == 'y': return self.value.year elif char == 'M': return self.value.month elif char == 'd': return self.value.day elif char == 'H': return self.value.hour elif char == 'h': return self.value.hour % 12 or 12 elif char == 'm': return self.value.minute elif char == 'a': return int(self.value.hour >= 12) # 0 for am, 1 for pm else: raise NotImplementedError(f"Not implemented: extracting {char!r} from {self.value!r}") def format_era(self, char: str, num: int) -> str: width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] era = int(self.value.year >= 0) return get_era_names(width, self.locale)[era] def format_year(self, char: str, num: int) -> str: value = self.value.year if char.isupper(): value = self.value.isocalendar()[0] year = self.format(value, num) if num == 2: year = year[-2:] return year def format_quarter(self, char: str, num: int) -> str: quarter = (self.value.month - 1) // 3 + 1 if num <= 2: return '%0*d' % (num, quarter) width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] context = {'Q': 'format', 'q': 'stand-alone'}[char] return get_quarter_names(width, context, self.locale)[quarter] def format_month(self, char: str, num: int) -> str: if num <= 2: return '%0*d' % (num, self.value.month) width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] context = {'M': 'format', 'L': 'stand-alone'}[char] return get_month_names(width, context, self.locale)[self.value.month] def format_week(self, char: str, num: int) -> str: if char.islower(): # week of year day_of_year = self.get_day_of_year() week = self.get_week_number(day_of_year) if week == 0: date = self.value - datetime.timedelta(days=day_of_year) week = self.get_week_number(self.get_day_of_year(date), date.weekday()) return self.format(week, num) else: # week of month week = self.get_week_number(self.value.day) if week == 0: date = self.value - datetime.timedelta(days=self.value.day) week = self.get_week_number(date.day, date.weekday()) return str(week) def format_weekday(self, char: str = 'E', num: int = 4) -> str: """ Return weekday from parsed datetime according to format pattern. >>> from datetime import date >>> format = DateTimeFormat(date(2016, 2, 28), Locale.parse('en_US')) >>> format.format_weekday() u'Sunday' 'E': Day of week - Use one through three letters for the abbreviated day name, four for the full (wide) name, five for the narrow name, or six for the short name. >>> format.format_weekday('E',2) u'Sun' 'e': Local day of week. Same as E except adds a numeric value that will depend on the local starting day of the week, using one or two letters. For this example, Monday is the first day of the week. >>> format.format_weekday('e',2) '01' 'c': Stand-Alone local day of week - Use one letter for the local numeric value (same as 'e'), three for the abbreviated day name, four for the full (wide) name, five for the narrow name, or six for the short name. >>> format.format_weekday('c',1) '1' :param char: pattern format character ('e','E','c') :param num: count of format character """ if num < 3: if char.islower(): value = 7 - self.locale.first_week_day + self.value.weekday() return self.format(value % 7 + 1, num) num = 3 weekday = self.value.weekday() width = {3: 'abbreviated', 4: 'wide', 5: 'narrow', 6: 'short'}[num] context = "stand-alone" if char == "c" else "format" return get_day_names(width, context, self.locale)[weekday] def format_day_of_year(self, num: int) -> str: return self.format(self.get_day_of_year(), num) def format_day_of_week_in_month(self) -> str: return str((self.value.day - 1) // 7 + 1) def format_period(self, char: str, num: int) -> str: """ Return period from parsed datetime according to format pattern. >>> from datetime import datetime, time >>> format = DateTimeFormat(time(13, 42), 'fi_FI') >>> format.format_period('a', 1) u'ip.' >>> format.format_period('b', 1) u'iltap.' >>> format.format_period('b', 4) u'iltapäivä' >>> format.format_period('B', 4) u'iltapäivällä' >>> format.format_period('B', 5) u'ip.' >>> format = DateTimeFormat(datetime(2022, 4, 28, 6, 27), 'zh_Hant') >>> format.format_period('a', 1) u'上午' >>> format.format_period('b', 1) u'清晨' >>> format.format_period('B', 1) u'清晨' :param char: pattern format character ('a', 'b', 'B') :param num: count of format character """ widths = [{3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)], 'wide', 'narrow', 'abbreviated'] if char == 'a': period = 'pm' if self.value.hour >= 12 else 'am' context = 'format' else: period = get_period_id(self.value, locale=self.locale) context = 'format' if char == 'B' else 'stand-alone' for width in widths: period_names = get_period_names(context=context, width=width, locale=self.locale) if period in period_names: return period_names[period] raise ValueError(f"Could not format period {period} in {self.locale}") def format_frac_seconds(self, num: int) -> str: """ Return fractional seconds. Rounds the time's microseconds to the precision given by the number \ of digits passed in. """ value = self.value.microsecond / 1000000 return self.format(round(value, num) * 10**num, num) def format_milliseconds_in_day(self, num): msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ self.value.minute * 60000 + self.value.hour * 3600000 return self.format(msecs, num) def format_timezone(self, char: str, num: int) -> str: width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)] # It could be that we only receive a time to format, but also have a # reference date which is important to distinguish between timezone # variants (summer/standard time) value = self.value if self.reference_date: value = datetime.datetime.combine(self.reference_date, self.value) if char == 'z': return get_timezone_name(value, width, locale=self.locale) elif char == 'Z': if num == 5: return get_timezone_gmt(value, width, locale=self.locale, return_z=True) return get_timezone_gmt(value, width, locale=self.locale) elif char == 'O': if num == 4: return get_timezone_gmt(value, width, locale=self.locale) # TODO: To add support for O:1 elif char == 'v': return get_timezone_name(value.tzinfo, width, locale=self.locale) elif char == 'V': if num == 1: return get_timezone_name(value.tzinfo, width, uncommon=True, locale=self.locale) elif num == 2: return get_timezone_name(value.tzinfo, locale=self.locale, return_zone=True) elif num == 3: return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True) return get_timezone_location(value.tzinfo, locale=self.locale) # Included additional elif condition to add support for 'Xx' in timezone format elif char == 'X': if num == 1: return get_timezone_gmt(value, width='iso8601_short', locale=self.locale, return_z=True) elif num in (2, 4): return get_timezone_gmt(value, width='short', locale=self.locale, return_z=True) elif num in (3, 5): return get_timezone_gmt(value, width='iso8601', locale=self.locale, return_z=True) elif char == 'x': if num == 1: return get_timezone_gmt(value, width='iso8601_short', locale=self.locale) elif num in (2, 4): return get_timezone_gmt(value, width='short', locale=self.locale) elif num in (3, 5): return get_timezone_gmt(value, width='iso8601', locale=self.locale) def format(self, value: SupportsInt, length: int) -> str: return '%0*d' % (length, value) def get_day_of_year(self, date: datetime.date | None = None) -> int: if date is None: date = self.value return (date - date.replace(month=1, day=1)).days + 1 def get_week_number(self, day_of_period: int, day_of_week: int | None = None) -> int: """Return the number of the week of a day within a period. This may be the week number in a year or the week number in a month. Usually this will return a value equal to or greater than 1, but if the first week of the period is so short that it actually counts as the last week of the previous period, this function will return 0. >>> date = datetime.date(2006, 1, 8) >>> DateTimeFormat(date, 'de_DE').get_week_number(6) 1 >>> DateTimeFormat(date, 'en_US').get_week_number(6) 2 :param day_of_period: the number of the day in the period (usually either the day of month or the day of year) :param day_of_week: the week day; if omitted, the week day of the current date is assumed """ if day_of_week is None: day_of_week = self.value.weekday() first_day = (day_of_week - self.locale.first_week_day - day_of_period + 1) % 7 if first_day < 0: first_day += 7 week_number = (day_of_period + first_day - 1) // 7 if 7 - first_day >= self.locale.min_week_days: week_number += 1 if self.locale.first_week_day == 0: # Correct the weeknumber in case of iso-calendar usage (first_week_day=0). # If the weeknumber exceeds the maximum number of weeks for the given year # we must count from zero.For example the above calculation gives week 53 # for 2018-12-31. By iso-calender definition 2018 has a max of 52 # weeks, thus the weeknumber must be 53-52=1. max_weeks = datetime.date(year=self.value.year, day=28, month=12).isocalendar()[1] if week_number > max_weeks: week_number -= max_weeks return week_number PATTERN_CHARS: dict[str, list[int] | None] = { 'G': [1, 2, 3, 4, 5], # era 'y': None, 'Y': None, 'u': None, # year 'Q': [1, 2, 3, 4, 5], 'q': [1, 2, 3, 4, 5], # quarter 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month 'w': [1, 2], 'W': [1], # week 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day 'E': [1, 2, 3, 4, 5, 6], 'e': [1, 2, 3, 4, 5, 6], 'c': [1, 3, 4, 5, 6], # week day 'a': [1, 2, 3, 4, 5], 'b': [1, 2, 3, 4, 5], 'B': [1, 2, 3, 4, 5], # period 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour 'm': [1, 2], # minute 's': [1, 2], 'S': None, 'A': None, # second 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4, 5], 'O': [1, 4], 'v': [1, 4], # zone 'V': [1, 2, 3, 4], 'x': [1, 2, 3, 4, 5], 'X': [1, 2, 3, 4, 5], # zone } #: The pattern characters declared in the Date Field Symbol Table #: (https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) #: in order of decreasing magnitude. PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" def parse_pattern(pattern: str | DateTimePattern) -> DateTimePattern: """Parse date, time, and datetime format patterns. >>> parse_pattern("MMMMd").format u'%(MMMM)s%(d)s' >>> parse_pattern("MMM d, yyyy").format u'%(MMM)s %(d)s, %(yyyy)s' Pattern can contain literal strings in single quotes: >>> parse_pattern("H:mm' Uhr 'z").format u'%(H)s:%(mm)s Uhr %(z)s' An actual single quote can be used by using two adjacent single quote characters: >>> parse_pattern("hh' o''clock'").format u"%(hh)s o'clock" :param pattern: the formatting pattern to parse """ if isinstance(pattern, DateTimePattern): return pattern return _cached_parse_pattern(pattern) @lru_cache(maxsize=1024) def _cached_parse_pattern(pattern: str) -> DateTimePattern: result = [] for tok_type, tok_value in tokenize_pattern(pattern): if tok_type == "chars": result.append(tok_value.replace('%', '%%')) elif tok_type == "field": fieldchar, fieldnum = tok_value limit = PATTERN_CHARS[fieldchar] if limit and fieldnum not in limit: raise ValueError(f"Invalid length for field: {fieldchar * fieldnum!r}") result.append('%%(%s)s' % (fieldchar * fieldnum)) else: raise NotImplementedError(f"Unknown token type: {tok_type}") return DateTimePattern(pattern, ''.join(result)) def tokenize_pattern(pattern: str) -> list[tuple[str, str | tuple[str, int]]]: """ Tokenize date format patterns. Returns a list of (token_type, token_value) tuples. ``token_type`` may be either "chars" or "field". For "chars" tokens, the value is the literal value. For "field" tokens, the value is a tuple of (field character, repetition count). :param pattern: Pattern string :type pattern: str :rtype: list[tuple] """ result = [] quotebuf = None charbuf = [] fieldchar = [''] fieldnum = [0] def append_chars(): result.append(('chars', ''.join(charbuf).replace('\0', "'"))) del charbuf[:] def append_field(): result.append(('field', (fieldchar[0], fieldnum[0]))) fieldchar[0] = '' fieldnum[0] = 0 for char in pattern.replace("''", '\0'): if quotebuf is None: if char == "'": # quote started if fieldchar[0]: append_field() elif charbuf: append_chars() quotebuf = [] elif char in PATTERN_CHARS: if charbuf: append_chars() if char == fieldchar[0]: fieldnum[0] += 1 else: if fieldchar[0]: append_field() fieldchar[0] = char fieldnum[0] = 1 else: if fieldchar[0]: append_field() charbuf.append(char) elif quotebuf is not None: if char == "'": # end of quote charbuf.extend(quotebuf) quotebuf = None else: # inside quote quotebuf.append(char) if fieldchar[0]: append_field() elif charbuf: append_chars() return result def untokenize_pattern(tokens: Iterable[tuple[str, str | tuple[str, int]]]) -> str: """ Turn a date format pattern token stream back into a string. This is the reverse operation of ``tokenize_pattern``. :type tokens: Iterable[tuple] :rtype: str """ output = [] for tok_type, tok_value in tokens: if tok_type == "field": output.append(tok_value[0] * tok_value[1]) elif tok_type == "chars": if not any(ch in PATTERN_CHARS for ch in tok_value): # No need to quote output.append(tok_value) else: output.append("'%s'" % tok_value.replace("'", "''")) return "".join(output) def split_interval_pattern(pattern: str) -> list[str]: """ Split an interval-describing datetime pattern into multiple pieces. > The pattern is then designed to be broken up into two pieces by determining the first repeating field. - https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats >>> split_interval_pattern(u'E d.M. \u2013 E d.M.') [u'E d.M. \u2013 ', 'E d.M.'] >>> split_interval_pattern("Y 'text' Y 'more text'") ["Y 'text '", "Y 'more text'"] >>> split_interval_pattern(u"E, MMM d \u2013 E") [u'E, MMM d \u2013 ', u'E'] >>> split_interval_pattern("MMM d") ['MMM d'] >>> split_interval_pattern("y G") ['y G'] >>> split_interval_pattern(u"MMM d \u2013 d") [u'MMM d \u2013 ', u'd'] :param pattern: Interval pattern string :return: list of "subpatterns" """ seen_fields = set() parts = [[]] for tok_type, tok_value in tokenize_pattern(pattern): if tok_type == "field": if tok_value[0] in seen_fields: # Repeated field parts.append([]) seen_fields.clear() seen_fields.add(tok_value[0]) parts[-1].append((tok_type, tok_value)) return [untokenize_pattern(tokens) for tokens in parts] def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields: bool = False) -> str | None: """ Find the closest match for the given datetime skeleton among the options given. This uses the rules outlined in the TR35 document. >>> match_skeleton('yMMd', ('yMd', 'yMMMd')) 'yMd' >>> match_skeleton('yMMd', ('jyMMd',), allow_different_fields=True) 'jyMMd' >>> match_skeleton('yMMd', ('qyMMd',), allow_different_fields=False) >>> match_skeleton('hmz', ('hmv',)) 'hmv' :param skeleton: The skeleton to match :type skeleton: str :param options: An iterable of other skeletons to match against :type options: Iterable[str] :return: The closest skeleton match, or if no match was found, None. :rtype: str|None """ # TODO: maybe implement pattern expansion? # Based on the implementation in # http://source.icu-project.org/repos/icu/icu4j/trunk/main/classes/core/src/com/ibm/icu/text/DateIntervalInfo.java # Filter out falsy values and sort for stability; when `interval_formats` is passed in, there may be a None key. options = sorted(option for option in options if option) if 'z' in skeleton and not any('z' in option for option in options): skeleton = skeleton.replace('z', 'v') get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get best_skeleton = None best_distance = None for option in options: get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get distance = 0 for field in PATTERN_CHARS: input_width = get_input_field_width(field, 0) opt_width = get_opt_field_width(field, 0) if input_width == opt_width: continue if opt_width == 0 or input_width == 0: if not allow_different_fields: # This one is not okay option = None break distance += 0x1000 # Magic weight constant for "entirely different fields" elif field == 'M' and ((input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2)): distance += 0x100 # Magic weight for "text turns into a number" else: distance += abs(input_width - opt_width) if not option: # We lost the option along the way (probably due to "allow_different_fields") continue if not best_skeleton or distance < best_distance: best_skeleton = option best_distance = distance if distance == 0: # Found a perfect match! break return best_skeleton babel-2.14.0/babel/util.py0000644000175000017500000001742414536056757014617 0ustar nileshnilesh""" babel.util ~~~~~~~~~~ Various utility classes and functions. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import codecs import collections import datetime import os import re import textwrap from collections.abc import Generator, Iterable from typing import IO, Any, TypeVar from babel import dates, localtime missing = object() _T = TypeVar("_T") def distinct(iterable: Iterable[_T]) -> Generator[_T, None, None]: """Yield all items in an iterable collection that are distinct. Unlike when using sets for a similar effect, the original ordering of the items in the collection is preserved by this function. >>> print(list(distinct([1, 2, 1, 3, 4, 4]))) [1, 2, 3, 4] >>> print(list(distinct('foobar'))) ['f', 'o', 'b', 'a', 'r'] :param iterable: the iterable collection providing the data """ seen = set() for item in iter(iterable): if item not in seen: yield item seen.add(item) # Regexp to match python magic encoding line PYTHON_MAGIC_COMMENT_re = re.compile( br'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE) def parse_encoding(fp: IO[bytes]) -> str | None: """Deduce the encoding of a source file from magic comment. It does this in the same way as the `Python interpreter`__ .. __: https://docs.python.org/3.4/reference/lexical_analysis.html#encoding-declarations The ``fp`` argument should be a seekable file object. (From Jeff Dairiki) """ pos = fp.tell() fp.seek(0) try: line1 = fp.readline() has_bom = line1.startswith(codecs.BOM_UTF8) if has_bom: line1 = line1[len(codecs.BOM_UTF8):] m = PYTHON_MAGIC_COMMENT_re.match(line1) if not m: try: import ast ast.parse(line1.decode('latin-1')) except (ImportError, SyntaxError, UnicodeEncodeError): # Either it's a real syntax error, in which case the source is # not valid python source, or line2 is a continuation of line1, # in which case we don't want to scan line2 for a magic # comment. pass else: line2 = fp.readline() m = PYTHON_MAGIC_COMMENT_re.match(line2) if has_bom: if m: magic_comment_encoding = m.group(1).decode('latin-1') if magic_comment_encoding != 'utf-8': raise SyntaxError(f"encoding problem: {magic_comment_encoding} with BOM") return 'utf-8' elif m: return m.group(1).decode('latin-1') else: return None finally: fp.seek(pos) PYTHON_FUTURE_IMPORT_re = re.compile( r'from\s+__future__\s+import\s+\(*(.+)\)*') def parse_future_flags(fp: IO[bytes], encoding: str = 'latin-1') -> int: """Parse the compiler flags by :mod:`__future__` from the given Python code. """ import __future__ pos = fp.tell() fp.seek(0) flags = 0 try: body = fp.read().decode(encoding) # Fix up the source to be (hopefully) parsable by regexpen. # This will likely do untoward things if the source code itself is broken. # (1) Fix `import (\n...` to be `import (...`. body = re.sub(r'import\s*\([\r\n]+', 'import (', body) # (2) Join line-ending commas with the next line. body = re.sub(r',\s*[\r\n]+', ', ', body) # (3) Remove backslash line continuations. body = re.sub(r'\\\s*[\r\n]+', ' ', body) for m in PYTHON_FUTURE_IMPORT_re.finditer(body): names = [x.strip().strip('()') for x in m.group(1).split(',')] for name in names: feature = getattr(__future__, name, None) if feature: flags |= feature.compiler_flag finally: fp.seek(pos) return flags def pathmatch(pattern: str, filename: str) -> bool: """Extended pathname pattern matching. This function is similar to what is provided by the ``fnmatch`` module in the Python standard library, but: * can match complete (relative or absolute) path names, and not just file names, and * also supports a convenience pattern ("**") to match files at any directory level. Examples: >>> pathmatch('**.py', 'bar.py') True >>> pathmatch('**.py', 'foo/bar/baz.py') True >>> pathmatch('**.py', 'templates/index.html') False >>> pathmatch('./foo/**.py', 'foo/bar/baz.py') True >>> pathmatch('./foo/**.py', 'bar/baz.py') False >>> pathmatch('^foo/**.py', 'foo/bar/baz.py') True >>> pathmatch('^foo/**.py', 'bar/baz.py') False >>> pathmatch('**/templates/*.html', 'templates/index.html') True >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') False :param pattern: the glob pattern :param filename: the path name of the file to match against """ symbols = { '?': '[^/]', '?/': '[^/]/', '*': '[^/]+', '*/': '[^/]+/', '**/': '(?:.+/)*?', '**': '(?:.+/)*?[^/]+', } if pattern.startswith('^'): buf = ['^'] pattern = pattern[1:] elif pattern.startswith('./'): buf = ['^'] pattern = pattern[2:] else: buf = [] for idx, part in enumerate(re.split('([?*]+/?)', pattern)): if idx % 2: buf.append(symbols[part]) elif part: buf.append(re.escape(part)) match = re.match(f"{''.join(buf)}$", filename.replace(os.sep, "/")) return match is not None class TextWrapper(textwrap.TextWrapper): wordsep_re = re.compile( r'(\s+|' # any whitespace r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))', # em-dash ) def wraptext(text: str, width: int = 70, initial_indent: str = '', subsequent_indent: str = '') -> list[str]: """Simple wrapper around the ``textwrap.wrap`` function in the standard library. This version does not wrap lines on hyphens in words. :param text: the text to wrap :param width: the maximum line width :param initial_indent: string that will be prepended to the first line of wrapped output :param subsequent_indent: string that will be prepended to all lines save the first of wrapped output """ wrapper = TextWrapper(width=width, initial_indent=initial_indent, subsequent_indent=subsequent_indent, break_long_words=False) return wrapper.wrap(text) # TODO (Babel 3.x): Remove this re-export odict = collections.OrderedDict class FixedOffsetTimezone(datetime.tzinfo): """Fixed offset in minutes east from UTC.""" def __init__(self, offset: float, name: str | None = None) -> None: self._offset = datetime.timedelta(minutes=offset) if name is None: name = 'Etc/GMT%+d' % offset self.zone = name def __str__(self) -> str: return self.zone def __repr__(self) -> str: return f'' def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta: return self._offset def tzname(self, dt: datetime.datetime) -> str: return self.zone def dst(self, dt: datetime.datetime) -> datetime.timedelta: return ZERO # Export the localtime functionality here because that's # where it was in the past. # TODO(3.0): remove these aliases UTC = dates.UTC LOCALTZ = dates.LOCALTZ get_localzone = localtime.get_localzone STDOFFSET = localtime.STDOFFSET DSTOFFSET = localtime.DSTOFFSET DSTDIFF = localtime.DSTDIFF ZERO = localtime.ZERO def _cmp(a: Any, b: Any): return (a > b) - (a < b) babel-2.14.0/babel/numbers.py0000644000175000017500000016710214536056757015314 0ustar nileshnilesh""" babel.numbers ~~~~~~~~~~~~~ Locale dependent formatting and parsing of numeric data. The default locale for the functions in this module is determined by the following environment variables, in that order: * ``LC_NUMERIC``, * ``LC_ALL``, and * ``LANG`` :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ # TODO: # Padding and rounding increments in pattern: # - https://www.unicode.org/reports/tr35/ (Appendix G.6) from __future__ import annotations import datetime import decimal import re import warnings from typing import TYPE_CHECKING, Any, cast, overload from babel.core import Locale, default_locale, get_global from babel.localedata import LocaleDataDict if TYPE_CHECKING: from typing_extensions import Literal LC_NUMERIC = default_locale('LC_NUMERIC') class UnknownCurrencyError(Exception): """Exception thrown when a currency is requested for which no data is available. """ def __init__(self, identifier: str) -> None: """Create the exception. :param identifier: the identifier string of the unsupported currency """ Exception.__init__(self, f"Unknown currency {identifier!r}.") #: The identifier of the locale that could not be found. self.identifier = identifier def list_currencies(locale: Locale | str | None = None) -> set[str]: """ Return a `set` of normalized currency codes. .. versionadded:: 2.5.0 :param locale: filters returned currency codes by the provided locale. Expected to be a locale instance or code. If no locale is provided, returns the list of all currencies from all locales. """ # Get locale-scoped currencies. if locale: return set(Locale.parse(locale).currencies) return set(get_global('all_currencies')) def validate_currency(currency: str, locale: Locale | str | None = None) -> None: """ Check the currency code is recognized by Babel. Accepts a ``locale`` parameter for fined-grained validation, working as the one defined above in ``list_currencies()`` method. Raises a `UnknownCurrencyError` exception if the currency is unknown to Babel. """ if currency not in list_currencies(locale): raise UnknownCurrencyError(currency) def is_currency(currency: str, locale: Locale | str | None = None) -> bool: """ Returns `True` only if a currency is recognized by Babel. This method always return a Boolean and never raise. """ if not currency or not isinstance(currency, str): return False try: validate_currency(currency, locale) except UnknownCurrencyError: return False return True def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None: """Returns the normalized identifier of any currency code. Accepts a ``locale`` parameter for fined-grained validation, working as the one defined above in ``list_currencies()`` method. Returns None if the currency is unknown to Babel. """ if isinstance(currency, str): currency = currency.upper() if not is_currency(currency, locale): return None return currency def get_currency_name( currency: str, count: float | decimal.Decimal | None = None, locale: Locale | str | None = LC_NUMERIC, ) -> str: """Return the name used by the locale for the specified currency. >>> get_currency_name('USD', locale='en_US') u'US Dollar' .. versionadded:: 0.9.4 :param currency: the currency code. :param count: the optional count. If provided the currency name will be pluralized to that number if possible. :param locale: the `Locale` object or locale identifier. """ loc = Locale.parse(locale) if count is not None: try: plural_form = loc.plural_form(count) except (OverflowError, ValueError): plural_form = 'other' plural_names = loc._data['currency_names_plural'] if currency in plural_names: currency_plural_names = plural_names[currency] if plural_form in currency_plural_names: return currency_plural_names[plural_form] if 'other' in currency_plural_names: return currency_plural_names['other'] return loc.currencies.get(currency, currency) def get_currency_symbol(currency: str, locale: Locale | str | None = LC_NUMERIC) -> str: """Return the symbol used by the locale for the specified currency. >>> get_currency_symbol('USD', locale='en_US') u'$' :param currency: the currency code. :param locale: the `Locale` object or locale identifier. """ return Locale.parse(locale).currency_symbols.get(currency, currency) def get_currency_precision(currency: str) -> int: """Return currency's precision. Precision is the number of decimals found after the decimal point in the currency's format pattern. .. versionadded:: 2.5.0 :param currency: the currency code. """ precisions = get_global('currency_fractions') return precisions.get(currency, precisions['DEFAULT'])[0] def get_currency_unit_pattern( currency: str, count: float | decimal.Decimal | None = None, locale: Locale | str | None = LC_NUMERIC, ) -> str: """ Return the unit pattern used for long display of a currency value for a given locale. This is a string containing ``{0}`` where the numeric part should be substituted and ``{1}`` where the currency long display name should be substituted. >>> get_currency_unit_pattern('USD', locale='en_US', count=10) u'{0} {1}' .. versionadded:: 2.7.0 :param currency: the currency code. :param count: the optional count. If provided the unit pattern for that number will be returned. :param locale: the `Locale` object or locale identifier. """ loc = Locale.parse(locale) if count is not None: plural_form = loc.plural_form(count) try: return loc._data['currency_unit_patterns'][plural_form] except LookupError: # Fall back to 'other' pass return loc._data['currency_unit_patterns']['other'] @overload def get_territory_currencies( territory: str, start_date: datetime.date | None = ..., end_date: datetime.date | None = ..., tender: bool = ..., non_tender: bool = ..., include_details: Literal[False] = ..., ) -> list[str]: ... # pragma: no cover @overload def get_territory_currencies( territory: str, start_date: datetime.date | None = ..., end_date: datetime.date | None = ..., tender: bool = ..., non_tender: bool = ..., include_details: Literal[True] = ..., ) -> list[dict[str, Any]]: ... # pragma: no cover def get_territory_currencies( territory: str, start_date: datetime.date | None = None, end_date: datetime.date | None = None, tender: bool = True, non_tender: bool = False, include_details: bool = False, ) -> list[str] | list[dict[str, Any]]: """Returns the list of currencies for the given territory that are valid for the given date range. In addition to that the currency database distinguishes between tender and non-tender currencies. By default only tender currencies are returned. The return value is a list of all currencies roughly ordered by the time of when the currency became active. The longer the currency is being in use the more to the left of the list it will be. The start date defaults to today. If no end date is given it will be the same as the start date. Otherwise a range can be defined. For instance this can be used to find the currencies in use in Austria between 1995 and 2011: >>> from datetime import date >>> get_territory_currencies('AT', date(1995, 1, 1), date(2011, 1, 1)) ['ATS', 'EUR'] Likewise it's also possible to find all the currencies in use on a single date: >>> get_territory_currencies('AT', date(1995, 1, 1)) ['ATS'] >>> get_territory_currencies('AT', date(2011, 1, 1)) ['EUR'] By default the return value only includes tender currencies. This however can be changed: >>> get_territory_currencies('US') ['USD'] >>> get_territory_currencies('US', tender=False, non_tender=True, ... start_date=date(2014, 1, 1)) ['USN', 'USS'] .. versionadded:: 2.0 :param territory: the name of the territory to find the currency for. :param start_date: the start date. If not given today is assumed. :param end_date: the end date. If not given the start date is assumed. :param tender: controls whether tender currencies should be included. :param non_tender: controls whether non-tender currencies should be included. :param include_details: if set to `True`, instead of returning currency codes the return value will be dictionaries with detail information. In that case each dictionary will have the keys ``'currency'``, ``'from'``, ``'to'``, and ``'tender'``. """ currencies = get_global('territory_currencies') if start_date is None: start_date = datetime.date.today() elif isinstance(start_date, datetime.datetime): start_date = start_date.date() if end_date is None: end_date = start_date elif isinstance(end_date, datetime.datetime): end_date = end_date.date() curs = currencies.get(territory.upper(), ()) # TODO: validate that the territory exists def _is_active(start, end): return (start is None or start <= end_date) and \ (end is None or end >= start_date) result = [] for currency_code, start, end, is_tender in curs: if start: start = datetime.date(*start) if end: end = datetime.date(*end) if ((is_tender and tender) or (not is_tender and non_tender)) and _is_active(start, end): if include_details: result.append({ 'currency': currency_code, 'from': start, 'to': end, 'tender': is_tender, }) else: result.append(currency_code) return result def _get_numbering_system(locale: Locale, numbering_system: Literal["default"] | str = "latn") -> str: if numbering_system == "default": return locale.default_numbering_system else: return numbering_system def _get_number_symbols( locale: Locale | str | None, *, numbering_system: Literal["default"] | str = "latn", ) -> LocaleDataDict: parsed_locale = Locale.parse(locale) numbering_system = _get_numbering_system(parsed_locale, numbering_system) try: return parsed_locale.number_symbols[numbering_system] except KeyError as error: raise UnsupportedNumberingSystemError(f"Unknown numbering system {numbering_system} for Locale {parsed_locale}.") from error class UnsupportedNumberingSystemError(Exception): """Exception thrown when an unsupported numbering system is requested for the given Locale.""" pass def get_decimal_symbol( locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the symbol used by the locale to separate decimal fractions. >>> get_decimal_symbol('en_US') u'.' >>> get_decimal_symbol('ar_EG', numbering_system='default') u'٫' >>> get_decimal_symbol('ar_EG', numbering_system='latn') u'.' :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ return _get_number_symbols(locale, numbering_system=numbering_system).get('decimal', '.') def get_plus_sign_symbol( locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the plus sign symbol used by the current locale. >>> get_plus_sign_symbol('en_US') u'+' >>> get_plus_sign_symbol('ar_EG', numbering_system='default') u'\u061c+' >>> get_plus_sign_symbol('ar_EG', numbering_system='latn') u'\u200e+' :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. """ return _get_number_symbols(locale, numbering_system=numbering_system).get('plusSign', '+') def get_minus_sign_symbol( locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the plus sign symbol used by the current locale. >>> get_minus_sign_symbol('en_US') u'-' >>> get_minus_sign_symbol('ar_EG', numbering_system='default') u'\u061c-' >>> get_minus_sign_symbol('ar_EG', numbering_system='latn') u'\u200e-' :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. """ return _get_number_symbols(locale, numbering_system=numbering_system).get('minusSign', '-') def get_exponential_symbol( locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the symbol used by the locale to separate mantissa and exponent. >>> get_exponential_symbol('en_US') u'E' >>> get_exponential_symbol('ar_EG', numbering_system='default') u'اس' >>> get_exponential_symbol('ar_EG', numbering_system='latn') u'E' :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. """ return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E') def get_group_symbol( locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the symbol used by the locale to separate groups of thousands. >>> get_group_symbol('en_US') u',' >>> get_group_symbol('ar_EG', numbering_system='default') u'٬' >>> get_group_symbol('ar_EG', numbering_system='latn') u',' :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. """ return _get_number_symbols(locale, numbering_system=numbering_system).get('group', ',') def get_infinity_symbol( locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the symbol used by the locale to represent infinity. >>> get_infinity_symbol('en_US') u'∞' >>> get_infinity_symbol('ar_EG', numbering_system='default') u'∞' >>> get_infinity_symbol('ar_EG', numbering_system='latn') u'∞' :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. """ return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞') def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str: """Return the given number formatted for a specific locale. >>> format_number(1099, locale='en_US') # doctest: +SKIP u'1,099' >>> format_number(1099, locale='de_DE') # doctest: +SKIP u'1.099' .. deprecated:: 2.6.0 Use babel.numbers.format_decimal() instead. :param number: the number to format :param locale: the `Locale` object or locale identifier """ warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning, stacklevel=2) return format_decimal(number, locale=locale) def get_decimal_precision(number: decimal.Decimal) -> int: """Return maximum precision of a decimal instance's fractional part. Precision is extracted from the fractional part only. """ # Copied from: https://github.com/mahmoud/boltons/pull/59 assert isinstance(number, decimal.Decimal) decimal_tuple = number.normalize().as_tuple() # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity) if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0: return 0 return abs(decimal_tuple.exponent) def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal: """Return minimal quantum of a number, as defined by precision.""" assert isinstance(precision, (int, decimal.Decimal)) return decimal.Decimal(10) ** (-precision) def format_decimal( number: float | decimal.Decimal | str, format: str | NumberPattern | None = None, locale: Locale | str | None = LC_NUMERIC, decimal_quantization: bool = True, group_separator: bool = True, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the given decimal number formatted for a specific locale. >>> format_decimal(1.2345, locale='en_US') u'1.234' >>> format_decimal(1.2346, locale='en_US') u'1.235' >>> format_decimal(-1.2346, locale='en_US') u'-1.235' >>> format_decimal(1.2345, locale='sv_SE') u'1,234' >>> format_decimal(1.2345, locale='de') u'1,234' >>> format_decimal(1.2345, locale='ar_EG', numbering_system='default') u'1٫234' >>> format_decimal(1.2345, locale='ar_EG', numbering_system='latn') u'1.234' The appropriate thousands grouping and the decimal separator are used for each locale: >>> format_decimal(12345.5, locale='en_US') u'12,345.5' By default the locale is allowed to truncate and round a high-precision number by forcing its format pattern onto the decimal part. You can bypass this behavior with the `decimal_quantization` parameter: >>> format_decimal(1.2346, locale='en_US') u'1.235' >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False) u'1.2346' >>> format_decimal(12345.67, locale='fr_CA', group_separator=False) u'12345,67' >>> format_decimal(12345.67, locale='en_US', group_separator=True) u'12,345.67' :param number: the number to format :param format: :param locale: the `Locale` object or locale identifier :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. :param group_separator: Boolean to switch group separator on/off in a locale's number format. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) if format is None: format = locale.decimal_formats[format] pattern = parse_pattern(format) return pattern.apply( number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) def format_compact_decimal( number: float | decimal.Decimal | str, *, format_type: Literal["short", "long"] = "short", locale: Locale | str | None = LC_NUMERIC, fraction_digits: int = 0, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return the given decimal number formatted for a specific locale in compact form. >>> format_compact_decimal(12345, format_type="short", locale='en_US') u'12K' >>> format_compact_decimal(12345, format_type="long", locale='en_US') u'12 thousand' >>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2) u'12.34K' >>> format_compact_decimal(1234567, format_type="short", locale="ja_JP") u'123万' >>> format_compact_decimal(2345678, format_type="long", locale="mk") u'2 милиони' >>> format_compact_decimal(21000000, format_type="long", locale="mk") u'21 милион' >>> format_compact_decimal(12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default') u'12٫34\xa0ألف' :param number: the number to format :param format_type: Compact format to use ("short" or "long") :param locale: the `Locale` object or locale identifier :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) compact_format = locale.compact_decimal_formats[format_type] number, format = _get_compact_format(number, compact_format, locale, fraction_digits) # Did not find a format, fall back. if format is None: format = locale.decimal_formats[None] pattern = parse_pattern(format) return pattern.apply(number, locale, decimal_quantization=False, numbering_system=numbering_system) def _get_compact_format( number: float | decimal.Decimal | str, compact_format: LocaleDataDict, locale: Locale, fraction_digits: int, ) -> tuple[decimal.Decimal, NumberPattern | None]: """Returns the number after dividing by the unit and the format pattern to use. The algorithm is described here: https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats. """ if not isinstance(number, decimal.Decimal): number = decimal.Decimal(str(number)) if number.is_nan() or number.is_infinite(): return number, None format = None for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): if abs(number) >= magnitude: # check the pattern using "other" as the amount format = compact_format["other"][str(magnitude)] pattern = parse_pattern(format).pattern # if the pattern is "0", we do not divide the number if pattern == "0": break # otherwise, we need to divide the number by the magnitude but remove zeros # equal to the number of 0's in the pattern minus 1 number = cast(decimal.Decimal, number / (magnitude // (10 ** (pattern.count("0") - 1)))) # round to the number of fraction digits requested rounded = round(number, fraction_digits) # if the remaining number is singular, use the singular format plural_form = locale.plural_form(abs(number)) if plural_form not in compact_format: plural_form = "other" if number == 1 and "1" in compact_format: plural_form = "1" format = compact_format[plural_form][str(magnitude)] number = rounded break return number, format class UnknownCurrencyFormatError(KeyError): """Exception raised when an unknown currency format is requested.""" def format_currency( number: float | decimal.Decimal | str, currency: str, format: str | NumberPattern | None = None, locale: Locale | str | None = LC_NUMERIC, currency_digits: bool = True, format_type: Literal["name", "standard", "accounting"] = "standard", decimal_quantization: bool = True, group_separator: bool = True, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return formatted currency value. >>> format_currency(1099.98, 'USD', locale='en_US') '$1,099.98' >>> format_currency(1099.98, 'USD', locale='es_CO') u'US$1.099,98' >>> format_currency(1099.98, 'EUR', locale='de_DE') u'1.099,98\\xa0\\u20ac' >>> format_currency(1099.98, 'EGP', locale='ar_EG', numbering_system='default') u'\u200f1٬099٫98\xa0ج.م.\u200f' The format can also be specified explicitly. The currency is placed with the '¤' sign. As the sign gets repeated the format expands (¤ being the symbol, ¤¤ is the currency abbreviation and ¤¤¤ is the full name of the currency): >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US') u'EUR 1,099.98' >>> format_currency(1099.98, 'EUR', u'#,##0.00 \xa4\xa4\xa4', locale='en_US') u'1,099.98 euros' Currencies usually have a specific number of decimal digits. This function favours that information over the given format: >>> format_currency(1099.98, 'JPY', locale='en_US') u'\\xa51,100' >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES') u'1.099,98' However, the number of decimal digits can be overridden from the currency information, by setting the last parameter to ``False``: >>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False) u'\\xa51,099.98' >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES', currency_digits=False) u'1.099,98' If a format is not specified the type of currency format to use from the locale can be specified: >>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard') u'\\u20ac1,099.98' When the given currency format type is not available, an exception is raised: >>> format_currency('1099.98', 'EUR', locale='root', format_type='unknown') Traceback (most recent call last): ... UnknownCurrencyFormatError: "'unknown' is not a known currency format type" >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False) u'$101299.98' >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True) u'$101,299.98' You can also pass format_type='name' to use long display names. The order of the number and currency name, along with the correct localized plural form of the currency name, is chosen according to locale: >>> format_currency(1, 'USD', locale='en_US', format_type='name') u'1.00 US dollar' >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name') u'1,099.98 US dollars' >>> format_currency(1099.98, 'USD', locale='ee', format_type='name') u'us ga dollar 1,099.98' By default the locale is allowed to truncate and round a high-precision number by forcing its format pattern onto the decimal part. You can bypass this behavior with the `decimal_quantization` parameter: >>> format_currency(1099.9876, 'USD', locale='en_US') u'$1,099.99' >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False) u'$1,099.9876' :param number: the number to format :param currency: the currency code :param format: the format string to use :param locale: the `Locale` object or locale identifier :param currency_digits: use the currency's natural number of decimal digits :param format_type: the currency format type to use :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. :param group_separator: Boolean to switch group separator on/off in a locale's number format. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ if format_type == 'name': return _format_currency_long_name(number, currency, format=format, locale=locale, currency_digits=currency_digits, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) locale = Locale.parse(locale) if format: pattern = parse_pattern(format) else: try: pattern = locale.currency_formats[format_type] except KeyError: raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") from None return pattern.apply( number, locale, currency=currency, currency_digits=currency_digits, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) def _format_currency_long_name( number: float | decimal.Decimal | str, currency: str, format: str | NumberPattern | None = None, locale: Locale | str | None = LC_NUMERIC, currency_digits: bool = True, format_type: Literal["name", "standard", "accounting"] = "standard", decimal_quantization: bool = True, group_separator: bool = True, *, numbering_system: Literal["default"] | str = "latn", ) -> str: # Algorithm described here: # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies locale = Locale.parse(locale) # Step 1. # There are no examples of items with explicit count (0 or 1) in current # locale data. So there is no point implementing that. # Step 2. # Correct number to numeric type, important for looking up plural rules: number_n = float(number) if isinstance(number, str) else number # Step 3. unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale) # Step 4. display_name = get_currency_name(currency, count=number_n, locale=locale) # Step 5. if not format: format = locale.decimal_formats[None] pattern = parse_pattern(format) number_part = pattern.apply( number, locale, currency=currency, currency_digits=currency_digits, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) return unit_pattern.format(number_part, display_name) def format_compact_currency( number: float | decimal.Decimal | str, currency: str, *, format_type: Literal["short"] = "short", locale: Locale | str | None = LC_NUMERIC, fraction_digits: int = 0, numbering_system: Literal["default"] | str = "latn", ) -> str: """Format a number as a currency value in compact form. >>> format_compact_currency(12345, 'USD', locale='en_US') u'$12K' >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2) u'$123.46M' >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1) '123,5\xa0Mio.\xa0€' :param number: the number to format :param currency: the currency code :param format_type: the compact format type to use. Defaults to "short". :param locale: the `Locale` object or locale identifier :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) try: compact_format = locale.compact_currency_formats[format_type] except KeyError as error: raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error number, format = _get_compact_format(number, compact_format, locale, fraction_digits) # Did not find a format, fall back. if format is None or "¤" not in str(format): # find first format that has a currency symbol for magnitude in compact_format['other']: format = compact_format['other'][magnitude].pattern if '¤' not in format: continue # remove characters that are not the currency symbol, 0's or spaces format = re.sub(r'[^0\s\¤]', '', format) # compress adjacent spaces into one format = re.sub(r'(\s)\s+', r'\1', format).strip() break if format is None: raise ValueError('No compact currency format found for the given number and locale.') pattern = parse_pattern(format) return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False, numbering_system=numbering_system) def format_percent( number: float | decimal.Decimal | str, format: str | NumberPattern | None = None, locale: Locale | str | None = LC_NUMERIC, decimal_quantization: bool = True, group_separator: bool = True, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return formatted percent value for a specific locale. >>> format_percent(0.34, locale='en_US') u'34%' >>> format_percent(25.1234, locale='en_US') u'2,512%' >>> format_percent(25.1234, locale='sv_SE') u'2\\xa0512\\xa0%' >>> format_percent(25.1234, locale='ar_EG', numbering_system='default') u'2٬512%' The format pattern can also be specified explicitly: >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US') u'25,123\u2030' By default the locale is allowed to truncate and round a high-precision number by forcing its format pattern onto the decimal part. You can bypass this behavior with the `decimal_quantization` parameter: >>> format_percent(23.9876, locale='en_US') u'2,399%' >>> format_percent(23.9876, locale='en_US', decimal_quantization=False) u'2,398.76%' >>> format_percent(229291.1234, locale='pt_BR', group_separator=False) u'22929112%' >>> format_percent(229291.1234, locale='pt_BR', group_separator=True) u'22.929.112%' :param number: the percent number to format :param format: :param locale: the `Locale` object or locale identifier :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. :param group_separator: Boolean to switch group separator on/off in a locale's number format. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) if not format: format = locale.percent_formats[None] pattern = parse_pattern(format) return pattern.apply( number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system, ) def format_scientific( number: float | decimal.Decimal | str, format: str | NumberPattern | None = None, locale: Locale | str | None = LC_NUMERIC, decimal_quantization: bool = True, *, numbering_system: Literal["default"] | str = "latn", ) -> str: """Return value formatted in scientific notation for a specific locale. >>> format_scientific(10000, locale='en_US') u'1E4' >>> format_scientific(10000, locale='ar_EG', numbering_system='default') u'1اس4' The format pattern can also be specified explicitly: >>> format_scientific(1234567, u'##0.##E00', locale='en_US') u'1.23E06' By default the locale is allowed to truncate and round a high-precision number by forcing its format pattern onto the decimal part. You can bypass this behavior with the `decimal_quantization` parameter: >>> format_scientific(1234.9876, u'#.##E0', locale='en_US') u'1.23E3' >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False) u'1.2349876E3' :param number: the number to format :param format: :param locale: the `Locale` object or locale identifier :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ locale = Locale.parse(locale) if not format: format = locale.scientific_formats[None] pattern = parse_pattern(format) return pattern.apply( number, locale, decimal_quantization=decimal_quantization, numbering_system=numbering_system) class NumberFormatError(ValueError): """Exception raised when a string cannot be parsed into a number.""" def __init__(self, message: str, suggestions: list[str] | None = None) -> None: super().__init__(message) #: a list of properly formatted numbers derived from the invalid input self.suggestions = suggestions def parse_number( string: str, locale: Locale | str | None = LC_NUMERIC, *, numbering_system: Literal["default"] | str = "latn", ) -> int: """Parse localized number string into an integer. >>> parse_number('1,099', locale='en_US') 1099 >>> parse_number('1.099', locale='de_DE') 1099 When the given string cannot be parsed, an exception is raised: >>> parse_number('1.099,98', locale='de') Traceback (most recent call last): ... NumberFormatError: '1.099,98' is not a valid number :param string: the string to parse :param locale: the `Locale` object or locale identifier :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :return: the parsed number :raise `NumberFormatError`: if the string can not be converted to a number :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. """ try: return int(string.replace(get_group_symbol(locale, numbering_system=numbering_system), '')) except ValueError as ve: raise NumberFormatError(f"{string!r} is not a valid number") from ve def parse_decimal( string: str, locale: Locale | str | None = LC_NUMERIC, strict: bool = False, *, numbering_system: Literal["default"] | str = "latn", ) -> decimal.Decimal: """Parse localized decimal string into a decimal. >>> parse_decimal('1,099.98', locale='en_US') Decimal('1099.98') >>> parse_decimal('1.099,98', locale='de') Decimal('1099.98') >>> parse_decimal('12 345,123', locale='ru') Decimal('12345.123') >>> parse_decimal('1٬099٫98', locale='ar_EG', numbering_system='default') Decimal('1099.98') When the given string cannot be parsed, an exception is raised: >>> parse_decimal('2,109,998', locale='de') Traceback (most recent call last): ... NumberFormatError: '2,109,998' is not a valid decimal number If `strict` is set to `True` and the given string contains a number formatted in an irregular way, an exception is raised: >>> parse_decimal('30.00', locale='de', strict=True) Traceback (most recent call last): ... NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'? >>> parse_decimal('0.00', locale='de', strict=True) Traceback (most recent call last): ... NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'? :param string: the string to parse :param locale: the `Locale` object or locale identifier :param strict: controls whether numbers formatted in a weird way are accepted or rejected :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise NumberFormatError: if the string can not be converted to a decimal number :raise UnsupportedNumberingSystemError: if the numbering system is not supported by the locale. """ locale = Locale.parse(locale) group_symbol = get_group_symbol(locale, numbering_system=numbering_system) decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system) if not strict and ( group_symbol == '\xa0' and # if the grouping symbol is U+00A0 NO-BREAK SPACE, group_symbol not in string and # and the string to be parsed does not contain it, ' ' in string # but it does contain a space instead, ): # ... it's reasonable to assume it is taking the place of the grouping symbol. string = string.replace(' ', group_symbol) try: parsed = decimal.Decimal(string.replace(group_symbol, '') .replace(decimal_symbol, '.')) except decimal.InvalidOperation as exc: raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc if strict and group_symbol in string: proper = format_decimal(parsed, locale=locale, decimal_quantization=False, numbering_system=numbering_system) if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): try: parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') .replace(group_symbol, '.')) except decimal.InvalidOperation as exc: raise NumberFormatError( f"{string!r} is not a properly formatted decimal number. " f"Did you mean {proper!r}?", suggestions=[proper], ) from exc else: proper_alt = format_decimal( parsed_alt, locale=locale, decimal_quantization=False, numbering_system=numbering_system, ) if proper_alt == proper: raise NumberFormatError( f"{string!r} is not a properly formatted decimal number. " f"Did you mean {proper!r}?", suggestions=[proper], ) else: raise NumberFormatError( f"{string!r} is not a properly formatted decimal number. " f"Did you mean {proper!r}? Or maybe {proper_alt!r}?", suggestions=[proper, proper_alt], ) return parsed def _remove_trailing_zeros_after_decimal(string: str, decimal_symbol: str) -> str: """ Remove trailing zeros from the decimal part of a numeric string. This function takes a string representing a numeric value and a decimal symbol. It removes any trailing zeros that appear after the decimal symbol in the number. If the decimal part becomes empty after removing trailing zeros, the decimal symbol is also removed. If the string does not contain the decimal symbol, it is returned unchanged. :param string: The numeric string from which to remove trailing zeros. :type string: str :param decimal_symbol: The symbol used to denote the decimal point. :type decimal_symbol: str :return: The numeric string with trailing zeros removed from its decimal part. :rtype: str Example: >>> _remove_trailing_zeros_after_decimal("123.4500", ".") '123.45' >>> _remove_trailing_zeros_after_decimal("100.000", ".") '100' >>> _remove_trailing_zeros_after_decimal("100", ".") '100' """ integer_part, _, decimal_part = string.partition(decimal_symbol) if decimal_part: decimal_part = decimal_part.rstrip("0") if decimal_part: return integer_part + decimal_symbol + decimal_part return integer_part return string PREFIX_END = r'[^0-9@#.,]' NUMBER_TOKEN = r'[0-9@#.,E+]' PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END NUMBER_PATTERN = r"(?P%s*)" % NUMBER_TOKEN SUFFIX_PATTERN = r"(?P.*)" number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}") def parse_grouping(p: str) -> tuple[int, int]: """Parse primary and secondary digit grouping >>> parse_grouping('##') (1000, 1000) >>> parse_grouping('#,###') (3, 3) >>> parse_grouping('#,####,###') (3, 4) """ width = len(p) g1 = p.rfind(',') if g1 == -1: return 1000, 1000 g1 = width - g1 - 1 g2 = p[:-g1 - 1].rfind(',') if g2 == -1: return g1, g1 g2 = width - g1 - g2 - 2 return g1, g2 def parse_pattern(pattern: NumberPattern | str) -> NumberPattern: """Parse number format patterns""" if isinstance(pattern, NumberPattern): return pattern def _match_number(pattern): rv = number_re.search(pattern) if rv is None: raise ValueError(f"Invalid number pattern {pattern!r}") return rv.groups() pos_pattern = pattern # Do we have a negative subpattern? if ';' in pattern: pos_pattern, neg_pattern = pattern.split(';', 1) pos_prefix, number, pos_suffix = _match_number(pos_pattern) neg_prefix, _, neg_suffix = _match_number(neg_pattern) else: pos_prefix, number, pos_suffix = _match_number(pos_pattern) neg_prefix = f"-{pos_prefix}" neg_suffix = pos_suffix if 'E' in number: number, exp = number.split('E', 1) else: exp = None if '@' in number and '.' in number and '0' in number: raise ValueError('Significant digit patterns can not contain "@" or "0"') if '.' in number: integer, fraction = number.rsplit('.', 1) else: integer = number fraction = '' def parse_precision(p): """Calculate the min and max allowed digits""" min = max = 0 for c in p: if c in '@0': min += 1 max += 1 elif c == '#': max += 1 elif c == ',': continue else: break return min, max int_prec = parse_precision(integer) frac_prec = parse_precision(fraction) if exp: exp_plus = exp.startswith('+') exp = exp.lstrip('+') exp_prec = parse_precision(exp) else: exp_plus = None exp_prec = None grouping = parse_grouping(integer) return NumberPattern(pattern, (pos_prefix, neg_prefix), (pos_suffix, neg_suffix), grouping, int_prec, frac_prec, exp_prec, exp_plus, number) class NumberPattern: def __init__( self, pattern: str, prefix: tuple[str, str], suffix: tuple[str, str], grouping: tuple[int, int], int_prec: tuple[int, int], frac_prec: tuple[int, int], exp_prec: tuple[int, int] | None, exp_plus: bool | None, number_pattern: str | None = None, ) -> None: # Metadata of the decomposed parsed pattern. self.pattern = pattern self.prefix = prefix self.suffix = suffix self.number_pattern = number_pattern self.grouping = grouping self.int_prec = int_prec self.frac_prec = frac_prec self.exp_prec = exp_prec self.exp_plus = exp_plus self.scale = self.compute_scale() def __repr__(self) -> str: return f"<{type(self).__name__} {self.pattern!r}>" def compute_scale(self) -> Literal[0, 2, 3]: """Return the scaling factor to apply to the number before rendering. Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is detected in the prefix or suffix of the pattern. Default is to not mess with the scale at all and keep it to 0. """ scale = 0 if '%' in ''.join(self.prefix + self.suffix): scale = 2 elif '‰' in ''.join(self.prefix + self.suffix): scale = 3 return scale def scientific_notation_elements( self, value: decimal.Decimal, locale: Locale | str | None, *, numbering_system: Literal["default"] | str = "latn", ) -> tuple[decimal.Decimal, int, str]: """ Returns normalized scientific notation components of a value. """ # Normalize value to only have one lead digit. exp = value.adjusted() value = value * get_decimal_quantum(exp) assert value.adjusted() == 0 # Shift exponent and value by the minimum number of leading digits # imposed by the rendering pattern. And always make that number # greater or equal to 1. lead_shift = max([1, min(self.int_prec)]) - 1 exp = exp - lead_shift value = value * get_decimal_quantum(-lead_shift) # Get exponent sign symbol. exp_sign = '' if exp < 0: exp_sign = get_minus_sign_symbol(locale, numbering_system=numbering_system) elif self.exp_plus: exp_sign = get_plus_sign_symbol(locale, numbering_system=numbering_system) # Normalize exponent value now that we have the sign. exp = abs(exp) return value, exp, exp_sign def apply( self, value: float | decimal.Decimal | str, locale: Locale | str | None, currency: str | None = None, currency_digits: bool = True, decimal_quantization: bool = True, force_frac: tuple[int, int] | None = None, group_separator: bool = True, *, numbering_system: Literal["default"] | str = "latn", ): """Renders into a string a number following the defined pattern. Forced decimal quantization is active by default so we'll produce a number string that is strictly following CLDR pattern definitions. :param value: The value to format. If this is not a Decimal object, it will be cast to one. :type value: decimal.Decimal|float|int :param locale: The locale to use for formatting. :type locale: str|babel.core.Locale :param currency: Which currency, if any, to format as. :type currency: str|None :param currency_digits: Whether or not to use the currency's precision. If false, the pattern's precision is used. :type currency_digits: bool :param decimal_quantization: Whether decimal numbers should be forcibly quantized to produce a formatted output strictly matching the CLDR definition for the locale. :type decimal_quantization: bool :param force_frac: DEPRECATED - a forced override for `self.frac_prec` for a single formatting invocation. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :return: Formatted decimal string. :rtype: str :raise UnsupportedNumberingSystemError: If the numbering system is not supported by the locale. """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) value = value.scaleb(self.scale) # Separate the absolute value from its sign. is_negative = int(value.is_signed()) value = abs(value).normalize() # Prepare scientific notation metadata. if self.exp_prec: value, exp, exp_sign = self.scientific_notation_elements(value, locale, numbering_system=numbering_system) # Adjust the precision of the fractional part and force it to the # currency's if necessary. if force_frac: # TODO (3.x?): Remove this parameter warnings.warn( 'The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning, stacklevel=2, ) frac_prec = force_frac elif currency and currency_digits: frac_prec = (get_currency_precision(currency), ) * 2 else: frac_prec = self.frac_prec # Bump decimal precision to the natural precision of the number if it # exceeds the one we're about to use. This adaptative precision is only # triggered if the decimal quantization is disabled or if a scientific # notation pattern has a missing mandatory fractional part (as in the # default '#E0' pattern). This special case has been extensively # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 . if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)): frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)])) # Render scientific notation. if self.exp_prec: number = ''.join([ self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system), get_exponential_symbol(locale, numbering_system=numbering_system), exp_sign, # type: ignore # exp_sign is always defined here self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale, numbering_system=numbering_system), # type: ignore # exp is always defined here ]) # Is it a significant digits pattern? elif '@' in self.pattern: text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system) if sep: number += get_decimal_symbol(locale, numbering_system=numbering_system) + b # A normal number pattern. else: number = self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system) retval = ''.join([ self.prefix[is_negative], number if self.number_pattern != '' else '', self.suffix[is_negative]]) if '¤' in retval and currency is not None: retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale)) retval = retval.replace('¤¤', currency.upper()) retval = retval.replace('¤', get_currency_symbol(currency, locale)) # remove single quotes around text, except for doubled single quotes # which are replaced with a single quote retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval) return retval # # This is one tricky piece of code. The idea is to rely as much as possible # on the decimal module to minimize the amount of code. # # Conceptually, the implementation of this method can be summarized in the # following steps: # # - Move or shift the decimal point (i.e. the exponent) so the maximum # amount of significant digits fall into the integer part (i.e. to the # left of the decimal point) # # - Round the number to the nearest integer, discarding all the fractional # part which contained extra digits to be eliminated # # - Convert the rounded integer to a string, that will contain the final # sequence of significant digits already trimmed to the maximum # # - Restore the original position of the decimal point, potentially # padding with zeroes on either side # def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str: exp = value.adjusted() scale = maximum - 1 - exp digits = str(value.scaleb(scale).quantize(decimal.Decimal(1))) if scale <= 0: result = digits + '0' * -scale else: intpart = digits[:-scale] i = len(intpart) j = i + max(minimum - i, 0) result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format( intpart=intpart or '0', pad='', fill=-min(exp + 1, 0), fracpart=digits[i:j], fracextra=digits[j:].rstrip('0'), ).rstrip('.') return result def _format_int( self, value: str, min: int, max: int, locale: Locale | str | None, *, numbering_system: Literal["default"] | str, ) -> str: width = len(value) if width < min: value = '0' * (min - width) + value gsize = self.grouping[0] ret = '' symbol = get_group_symbol(locale, numbering_system=numbering_system) while len(value) > gsize: ret = symbol + value[-gsize:] + ret value = value[:-gsize] gsize = self.grouping[1] return value + ret def _quantize_value( self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool, *, numbering_system: Literal["default"] | str, ) -> str: # If the number is +/-Infinity, we can't quantize it if value.is_infinite(): return get_infinity_symbol(locale, numbering_system=numbering_system) quantum = get_decimal_quantum(frac_prec[1]) rounded = value.quantize(quantum) a, sep, b = f"{rounded:f}".partition(".") integer_part = a if group_separator: integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale, numbering_system=numbering_system) number = integer_part + self._format_frac(b or '0', locale=locale, force_frac=frac_prec, numbering_system=numbering_system) return number def _format_frac( self, value: str, locale: Locale | str | None, force_frac: tuple[int, int] | None = None, *, numbering_system: Literal["default"] | str, ) -> str: min, max = force_frac or self.frac_prec if len(value) < min: value += ('0' * (min - len(value))) if max == 0 or (min == 0 and int(value) == 0): return '' while len(value) > min and value[-1] == '0': value = value[:-1] return get_decimal_symbol(locale, numbering_system=numbering_system) + value babel-2.14.0/babel/languages.py0000644000175000017500000000543414536056757015606 0ustar nileshnileshfrom __future__ import annotations from babel.core import get_global def get_official_languages(territory: str, regional: bool = False, de_facto: bool = False) -> tuple[str, ...]: """ Get the official language(s) for the given territory. The language codes, if any are known, are returned in order of descending popularity. If the `regional` flag is set, then languages which are regionally official are also returned. If the `de_facto` flag is set, then languages which are "de facto" official are also returned. .. warning:: Note that the data is as up to date as the current version of the CLDR used by Babel. If you need scientifically accurate information, use another source! :param territory: Territory code :type territory: str :param regional: Whether to return regionally official languages too :type regional: bool :param de_facto: Whether to return de-facto official languages too :type de_facto: bool :return: Tuple of language codes :rtype: tuple[str] """ territory = str(territory).upper() allowed_stati = {"official"} if regional: allowed_stati.add("official_regional") if de_facto: allowed_stati.add("de_facto_official") languages = get_global("territory_languages").get(territory, {}) pairs = [ (info['population_percent'], language) for language, info in languages.items() if info.get('official_status') in allowed_stati ] pairs.sort(reverse=True) return tuple(lang for _, lang in pairs) def get_territory_language_info(territory: str) -> dict[str, dict[str, float | str | None]]: """ Get a dictionary of language information for a territory. The dictionary is keyed by language code; the values are dicts with more information. The following keys are currently known for the values: * `population_percent`: The percentage of the territory's population speaking the language. * `official_status`: An optional string describing the officiality status of the language. Known values are "official", "official_regional" and "de_facto_official". .. warning:: Note that the data is as up to date as the current version of the CLDR used by Babel. If you need scientifically accurate information, use another source! .. note:: Note that the format of the dict returned may change between Babel versions. See https://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html :param territory: Territory code :type territory: str :return: Language information dictionary :rtype: dict[str, dict] """ territory = str(territory).upper() return get_global("territory_languages").get(territory, {}).copy() babel-2.14.0/babel/messages/0000755000175000017500000000000014536056757015067 5ustar nileshnileshbabel-2.14.0/babel/messages/setuptools_frontend.py0000644000175000017500000000663514536056757021573 0ustar nileshnileshfrom __future__ import annotations from babel.messages import frontend try: # See: https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html from setuptools import Command try: from setuptools.errors import BaseError, OptionError, SetupError except ImportError: # Error aliases only added in setuptools 59 (2021-11). OptionError = SetupError = BaseError = Exception except ImportError: from distutils.cmd import Command from distutils.errors import DistutilsSetupError as SetupError def check_message_extractors(dist, name, value): """Validate the ``message_extractors`` keyword argument to ``setup()``. :param dist: the distutils/setuptools ``Distribution`` object :param name: the name of the keyword argument (should always be "message_extractors") :param value: the value of the keyword argument :raise `DistutilsSetupError`: if the value is not valid """ assert name == "message_extractors" if not isinstance(value, dict): raise SetupError( 'the value of the "message_extractors" parameter must be a dictionary', ) class compile_catalog(frontend.CompileCatalog, Command): """Catalog compilation command for use in ``setup.py`` scripts. If correctly installed, this command is available to Setuptools-using setup scripts automatically. For projects using plain old ``distutils``, the command needs to be registered explicitly in ``setup.py``:: from babel.messages.setuptools_frontend import compile_catalog setup( ... cmdclass = {'compile_catalog': compile_catalog} ) .. versionadded:: 0.9 """ class extract_messages(frontend.ExtractMessages, Command): """Message extraction command for use in ``setup.py`` scripts. If correctly installed, this command is available to Setuptools-using setup scripts automatically. For projects using plain old ``distutils``, the command needs to be registered explicitly in ``setup.py``:: from babel.messages.setuptools_frontend import extract_messages setup( ... cmdclass = {'extract_messages': extract_messages} ) """ class init_catalog(frontend.InitCatalog, Command): """New catalog initialization command for use in ``setup.py`` scripts. If correctly installed, this command is available to Setuptools-using setup scripts automatically. For projects using plain old ``distutils``, the command needs to be registered explicitly in ``setup.py``:: from babel.messages.setuptools_frontend import init_catalog setup( ... cmdclass = {'init_catalog': init_catalog} ) """ class update_catalog(frontend.UpdateCatalog, Command): """Catalog merging command for use in ``setup.py`` scripts. If correctly installed, this command is available to Setuptools-using setup scripts automatically. For projects using plain old ``distutils``, the command needs to be registered explicitly in ``setup.py``:: from babel.messages.setuptools_frontend import update_catalog setup( ... cmdclass = {'update_catalog': update_catalog} ) .. versionadded:: 0.9 """ COMMANDS = { "compile_catalog": compile_catalog, "extract_messages": extract_messages, "init_catalog": init_catalog, "update_catalog": update_catalog, } babel-2.14.0/babel/messages/frontend.py0000644000175000017500000012122214536056757017260 0ustar nileshnilesh""" babel.messages.frontend ~~~~~~~~~~~~~~~~~~~~~~~ Frontends for the message extraction functionality. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import datetime import fnmatch import logging import optparse import os import re import shutil import sys import tempfile from collections import OrderedDict from configparser import RawConfigParser from io import StringIO from typing import Iterable from babel import Locale, localedata from babel import __version__ as VERSION from babel.core import UnknownLocaleError from babel.messages.catalog import DEFAULT_HEADER, Catalog from babel.messages.extract import ( DEFAULT_KEYWORDS, DEFAULT_MAPPING, check_and_call_extract_file, extract_from_dir, ) from babel.messages.mofile import write_mo from babel.messages.pofile import read_po, write_po from babel.util import LOCALTZ log = logging.getLogger('babel') class BaseError(Exception): pass class OptionError(BaseError): pass class SetupError(BaseError): pass def listify_value(arg, split=None): """ Make a list out of an argument. Values from `distutils` argument parsing are always single strings; values from `optparse` parsing may be lists of strings that may need to be further split. No matter the input, this function returns a flat list of whitespace-trimmed strings, with `None` values filtered out. >>> listify_value("foo bar") ['foo', 'bar'] >>> listify_value(["foo bar"]) ['foo', 'bar'] >>> listify_value([["foo"], "bar"]) ['foo', 'bar'] >>> listify_value([["foo"], ["bar", None, "foo"]]) ['foo', 'bar', 'foo'] >>> listify_value("foo, bar, quux", ",") ['foo', 'bar', 'quux'] :param arg: A string or a list of strings :param split: The argument to pass to `str.split()`. :return: """ out = [] if not isinstance(arg, (list, tuple)): arg = [arg] for val in arg: if val is None: continue if isinstance(val, (list, tuple)): out.extend(listify_value(val, split=split)) continue out.extend(s.strip() for s in str(val).split(split)) assert all(isinstance(val, str) for val in out) return out class CommandMixin: # This class is a small shim between Distutils commands and # optparse option parsing in the frontend command line. #: Option name to be input as `args` on the script command line. as_args = None #: Options which allow multiple values. #: This is used by the `optparse` transmogrification code. multiple_value_options = () #: Options which are booleans. #: This is used by the `optparse` transmogrification code. # (This is actually used by distutils code too, but is never # declared in the base class.) boolean_options = () #: Option aliases, to retain standalone command compatibility. #: Distutils does not support option aliases, but optparse does. #: This maps the distutils argument name to an iterable of aliases #: that are usable with optparse. option_aliases = {} #: Choices for options that needed to be restricted to specific #: list of choices. option_choices = {} #: Log object. To allow replacement in the script command line runner. log = log def __init__(self, dist=None): # A less strict version of distutils' `__init__`. self.distribution = dist self.initialize_options() self._dry_run = None self.verbose = False self.force = None self.help = 0 self.finalized = 0 def initialize_options(self): pass def ensure_finalized(self): if not self.finalized: self.finalize_options() self.finalized = 1 def finalize_options(self): raise RuntimeError( f"abstract method -- subclass {self.__class__} must override", ) class CompileCatalog(CommandMixin): description = 'compile message catalogs to binary MO files' user_options = [ ('domain=', 'D', "domains of PO files (space separated list, default 'messages')"), ('directory=', 'd', 'path to base directory containing the catalogs'), ('input-file=', 'i', 'name of the input file'), ('output-file=', 'o', "name of the output file (default " "'//LC_MESSAGES/.mo')"), ('locale=', 'l', 'locale of the catalog to compile'), ('use-fuzzy', 'f', 'also include fuzzy translations'), ('statistics', None, 'print statistics about translations'), ] boolean_options = ['use-fuzzy', 'statistics'] def initialize_options(self): self.domain = 'messages' self.directory = None self.input_file = None self.output_file = None self.locale = None self.use_fuzzy = False self.statistics = False def finalize_options(self): self.domain = listify_value(self.domain) if not self.input_file and not self.directory: raise OptionError('you must specify either the input file or the base directory') if not self.output_file and not self.directory: raise OptionError('you must specify either the output file or the base directory') def run(self): n_errors = 0 for domain in self.domain: for errors in self._run_domain(domain).values(): n_errors += len(errors) if n_errors: self.log.error('%d errors encountered.', n_errors) return (1 if n_errors else 0) def _run_domain(self, domain): po_files = [] mo_files = [] if not self.input_file: if self.locale: po_files.append((self.locale, os.path.join(self.directory, self.locale, 'LC_MESSAGES', f"{domain}.po"))) mo_files.append(os.path.join(self.directory, self.locale, 'LC_MESSAGES', f"{domain}.mo")) else: for locale in os.listdir(self.directory): po_file = os.path.join(self.directory, locale, 'LC_MESSAGES', f"{domain}.po") if os.path.exists(po_file): po_files.append((locale, po_file)) mo_files.append(os.path.join(self.directory, locale, 'LC_MESSAGES', f"{domain}.mo")) else: po_files.append((self.locale, self.input_file)) if self.output_file: mo_files.append(self.output_file) else: mo_files.append(os.path.join(self.directory, self.locale, 'LC_MESSAGES', f"{domain}.mo")) if not po_files: raise OptionError('no message catalogs found') catalogs_and_errors = {} for idx, (locale, po_file) in enumerate(po_files): mo_file = mo_files[idx] with open(po_file, 'rb') as infile: catalog = read_po(infile, locale) if self.statistics: translated = 0 for message in list(catalog)[1:]: if message.string: translated += 1 percentage = 0 if len(catalog): percentage = translated * 100 // len(catalog) self.log.info( '%d of %d messages (%d%%) translated in %s', translated, len(catalog), percentage, po_file, ) if catalog.fuzzy and not self.use_fuzzy: self.log.info('catalog %s is marked as fuzzy, skipping', po_file) continue catalogs_and_errors[catalog] = catalog_errors = list(catalog.check()) for message, errors in catalog_errors: for error in errors: self.log.error( 'error: %s:%d: %s', po_file, message.lineno, error, ) self.log.info('compiling catalog %s to %s', po_file, mo_file) with open(mo_file, 'wb') as outfile: write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) return catalogs_and_errors def _make_directory_filter(ignore_patterns): """ Build a directory_filter function based on a list of ignore patterns. """ def cli_directory_filter(dirname): basename = os.path.basename(dirname) return not any( fnmatch.fnmatch(basename, ignore_pattern) for ignore_pattern in ignore_patterns ) return cli_directory_filter class ExtractMessages(CommandMixin): description = 'extract localizable strings from the project code' user_options = [ ('charset=', None, 'charset to use in the output file (default "utf-8")'), ('keywords=', 'k', 'space-separated list of keywords to look for in addition to the ' 'defaults (may be repeated multiple times)'), ('no-default-keywords', None, 'do not include the default keywords'), ('mapping-file=', 'F', 'path to the mapping configuration file'), ('no-location', None, 'do not include location comments with filename and line number'), ('add-location=', None, 'location lines format. If it is not given or "full", it generates ' 'the lines with both file name and line number. If it is "file", ' 'the line number part is omitted. If it is "never", it completely ' 'suppresses the lines (same as --no-location).'), ('omit-header', None, 'do not include msgid "" entry in header'), ('output-file=', 'o', 'name of the output file'), ('width=', 'w', 'set output line width (default 76)'), ('no-wrap', None, 'do not break long message lines, longer than the output line width, ' 'into several lines'), ('sort-output', None, 'generate sorted output (default False)'), ('sort-by-file', None, 'sort output by file location (default False)'), ('msgid-bugs-address=', None, 'set report address for msgid'), ('copyright-holder=', None, 'set copyright holder in output'), ('project=', None, 'set project name in output'), ('version=', None, 'set project version in output'), ('add-comments=', 'c', 'place comment block with TAG (or those preceding keyword lines) in ' 'output file. Separate multiple TAGs with commas(,)'), # TODO: Support repetition of this argument ('strip-comments', 's', 'strip the comment TAGs from the comments.'), ('input-paths=', None, 'files or directories that should be scanned for messages. Separate multiple ' 'files or directories with commas(,)'), # TODO: Support repetition of this argument ('input-dirs=', None, # TODO (3.x): Remove me. 'alias for input-paths (does allow files as well as directories).'), ('ignore-dirs=', None, 'Patterns for directories to ignore when scanning for messages. ' 'Separate multiple patterns with spaces (default ".* ._")'), ('header-comment=', None, 'header comment for the catalog'), ('last-translator=', None, 'set the name and email of the last translator in output'), ] boolean_options = [ 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', 'sort-output', 'sort-by-file', 'strip-comments', ] as_args = 'input-paths' multiple_value_options = ( 'add-comments', 'keywords', 'ignore-dirs', ) option_aliases = { 'keywords': ('--keyword',), 'mapping-file': ('--mapping',), 'output-file': ('--output',), 'strip-comments': ('--strip-comment-tags',), 'last-translator': ('--last-translator',), } option_choices = { 'add-location': ('full', 'file', 'never'), } def initialize_options(self): self.charset = 'utf-8' self.keywords = None self.no_default_keywords = False self.mapping_file = None self.no_location = False self.add_location = None self.omit_header = False self.output_file = None self.input_dirs = None self.input_paths = None self.width = None self.no_wrap = False self.sort_output = False self.sort_by_file = False self.msgid_bugs_address = None self.copyright_holder = None self.project = None self.version = None self.add_comments = None self.strip_comments = False self.include_lineno = True self.ignore_dirs = None self.header_comment = None self.last_translator = None def finalize_options(self): if self.input_dirs: if not self.input_paths: self.input_paths = self.input_dirs else: raise OptionError( 'input-dirs and input-paths are mutually exclusive', ) keywords = {} if self.no_default_keywords else DEFAULT_KEYWORDS.copy() keywords.update(parse_keywords(listify_value(self.keywords))) self.keywords = keywords if not self.keywords: raise OptionError( 'you must specify new keywords if you disable the default ones', ) if not self.output_file: raise OptionError('no output file specified') if self.no_wrap and self.width: raise OptionError( "'--no-wrap' and '--width' are mutually exclusive", ) if not self.no_wrap and not self.width: self.width = 76 elif self.width is not None: self.width = int(self.width) if self.sort_output and self.sort_by_file: raise OptionError( "'--sort-output' and '--sort-by-file' are mutually exclusive", ) if self.input_paths: if isinstance(self.input_paths, str): self.input_paths = re.split(r',\s*', self.input_paths) elif self.distribution is not None: self.input_paths = dict.fromkeys([ k.split('.', 1)[0] for k in (self.distribution.packages or ()) ]).keys() else: self.input_paths = [] if not self.input_paths: raise OptionError("no input files or directories specified") for path in self.input_paths: if not os.path.exists(path): raise OptionError(f"Input path: {path} does not exist") self.add_comments = listify_value(self.add_comments or (), ",") if self.distribution: if not self.project: self.project = self.distribution.get_name() if not self.version: self.version = self.distribution.get_version() if self.add_location == 'never': self.no_location = True elif self.add_location == 'file': self.include_lineno = False ignore_dirs = listify_value(self.ignore_dirs) if ignore_dirs: self.directory_filter = _make_directory_filter(self.ignore_dirs) else: self.directory_filter = None def _build_callback(self, path: str): def callback(filename: str, method: str, options: dict): if method == 'ignore': return # If we explicitly provide a full filepath, just use that. # Otherwise, path will be the directory path and filename # is the relative path from that dir to the file. # So we can join those to get the full filepath. if os.path.isfile(path): filepath = path else: filepath = os.path.normpath(os.path.join(path, filename)) optstr = '' if options: opt_values = ", ".join(f'{k}="{v}"' for k, v in options.items()) optstr = f" ({opt_values})" self.log.info('extracting messages from %s%s', filepath, optstr) return callback def run(self): mappings = self._get_mappings() with open(self.output_file, 'wb') as outfile: catalog = Catalog(project=self.project, version=self.version, msgid_bugs_address=self.msgid_bugs_address, copyright_holder=self.copyright_holder, charset=self.charset, header_comment=(self.header_comment or DEFAULT_HEADER), last_translator=self.last_translator) for path, method_map, options_map in mappings: callback = self._build_callback(path) if os.path.isfile(path): current_dir = os.getcwd() extracted = check_and_call_extract_file( path, method_map, options_map, callback, self.keywords, self.add_comments, self.strip_comments, current_dir, ) else: extracted = extract_from_dir( path, method_map, options_map, keywords=self.keywords, comment_tags=self.add_comments, callback=callback, strip_comment_tags=self.strip_comments, directory_filter=self.directory_filter, ) for filename, lineno, message, comments, context in extracted: if os.path.isfile(path): filepath = filename # already normalized else: filepath = os.path.normpath(os.path.join(path, filename)) catalog.add(message, None, [(filepath, lineno)], auto_comments=comments, context=context) self.log.info('writing PO template file to %s', self.output_file) write_po(outfile, catalog, width=self.width, no_location=self.no_location, omit_header=self.omit_header, sort_output=self.sort_output, sort_by_file=self.sort_by_file, include_lineno=self.include_lineno) def _get_mappings(self): mappings = [] if self.mapping_file: with open(self.mapping_file) as fileobj: method_map, options_map = parse_mapping(fileobj) for path in self.input_paths: mappings.append((path, method_map, options_map)) elif getattr(self.distribution, 'message_extractors', None): message_extractors = self.distribution.message_extractors for path, mapping in message_extractors.items(): if isinstance(mapping, str): method_map, options_map = parse_mapping(StringIO(mapping)) else: method_map, options_map = [], {} for pattern, method, options in mapping: method_map.append((pattern, method)) options_map[pattern] = options or {} mappings.append((path, method_map, options_map)) else: for path in self.input_paths: mappings.append((path, DEFAULT_MAPPING, {})) return mappings class InitCatalog(CommandMixin): description = 'create a new catalog based on a POT file' user_options = [ ('domain=', 'D', "domain of PO file (default 'messages')"), ('input-file=', 'i', 'name of the input file'), ('output-dir=', 'd', 'path to output directory'), ('output-file=', 'o', "name of the output file (default " "'//LC_MESSAGES/.po')"), ('locale=', 'l', 'locale for the new localized catalog'), ('width=', 'w', 'set output line width (default 76)'), ('no-wrap', None, 'do not break long message lines, longer than the output line width, ' 'into several lines'), ] boolean_options = ['no-wrap'] def initialize_options(self): self.output_dir = None self.output_file = None self.input_file = None self.locale = None self.domain = 'messages' self.no_wrap = False self.width = None def finalize_options(self): if not self.input_file: raise OptionError('you must specify the input file') if not self.locale: raise OptionError('you must provide a locale for the new catalog') try: self._locale = Locale.parse(self.locale) except UnknownLocaleError as e: raise OptionError(e) from e if not self.output_file and not self.output_dir: raise OptionError('you must specify the output directory') if not self.output_file: self.output_file = os.path.join(self.output_dir, self.locale, 'LC_MESSAGES', f"{self.domain}.po") if not os.path.exists(os.path.dirname(self.output_file)): os.makedirs(os.path.dirname(self.output_file)) if self.no_wrap and self.width: raise OptionError("'--no-wrap' and '--width' are mutually exclusive") if not self.no_wrap and not self.width: self.width = 76 elif self.width is not None: self.width = int(self.width) def run(self): self.log.info( 'creating catalog %s based on %s', self.output_file, self.input_file, ) with open(self.input_file, 'rb') as infile: # Although reading from the catalog template, read_po must be fed # the locale in order to correctly calculate plurals catalog = read_po(infile, locale=self.locale) catalog.locale = self._locale catalog.revision_date = datetime.datetime.now(LOCALTZ) catalog.fuzzy = False with open(self.output_file, 'wb') as outfile: write_po(outfile, catalog, width=self.width) class UpdateCatalog(CommandMixin): description = 'update message catalogs from a POT file' user_options = [ ('domain=', 'D', "domain of PO file (default 'messages')"), ('input-file=', 'i', 'name of the input file'), ('output-dir=', 'd', 'path to base directory containing the catalogs'), ('output-file=', 'o', "name of the output file (default " "'//LC_MESSAGES/.po')"), ('omit-header', None, "do not include msgid "" entry in header"), ('locale=', 'l', 'locale of the catalog to compile'), ('width=', 'w', 'set output line width (default 76)'), ('no-wrap', None, 'do not break long message lines, longer than the output line width, ' 'into several lines'), ('ignore-obsolete=', None, 'whether to omit obsolete messages from the output'), ('init-missing=', None, 'if any output files are missing, initialize them first'), ('no-fuzzy-matching', 'N', 'do not use fuzzy matching'), ('update-header-comment', None, 'update target header comment'), ('previous', None, 'keep previous msgids of translated messages'), ('check=', None, 'don\'t update the catalog, just return the status. Return code 0 ' 'means nothing would change. Return code 1 means that the catalog ' 'would be updated'), ('ignore-pot-creation-date=', None, 'ignore changes to POT-Creation-Date when updating or checking'), ] boolean_options = [ 'omit-header', 'no-wrap', 'ignore-obsolete', 'init-missing', 'no-fuzzy-matching', 'previous', 'update-header-comment', 'check', 'ignore-pot-creation-date', ] def initialize_options(self): self.domain = 'messages' self.input_file = None self.output_dir = None self.output_file = None self.omit_header = False self.locale = None self.width = None self.no_wrap = False self.ignore_obsolete = False self.init_missing = False self.no_fuzzy_matching = False self.update_header_comment = False self.previous = False self.check = False self.ignore_pot_creation_date = False def finalize_options(self): if not self.input_file: raise OptionError('you must specify the input file') if not self.output_file and not self.output_dir: raise OptionError('you must specify the output file or directory') if self.output_file and not self.locale: raise OptionError('you must specify the locale') if self.init_missing: if not self.locale: raise OptionError( 'you must specify the locale for ' 'the init-missing option to work', ) try: self._locale = Locale.parse(self.locale) except UnknownLocaleError as e: raise OptionError(e) from e else: self._locale = None if self.no_wrap and self.width: raise OptionError("'--no-wrap' and '--width' are mutually exclusive") if not self.no_wrap and not self.width: self.width = 76 elif self.width is not None: self.width = int(self.width) if self.no_fuzzy_matching and self.previous: self.previous = False def run(self): check_status = {} po_files = [] if not self.output_file: if self.locale: po_files.append((self.locale, os.path.join(self.output_dir, self.locale, 'LC_MESSAGES', f"{self.domain}.po"))) else: for locale in os.listdir(self.output_dir): po_file = os.path.join(self.output_dir, locale, 'LC_MESSAGES', f"{self.domain}.po") if os.path.exists(po_file): po_files.append((locale, po_file)) else: po_files.append((self.locale, self.output_file)) if not po_files: raise OptionError('no message catalogs found') domain = self.domain if not domain: domain = os.path.splitext(os.path.basename(self.input_file))[0] with open(self.input_file, 'rb') as infile: template = read_po(infile) for locale, filename in po_files: if self.init_missing and not os.path.exists(filename): if self.check: check_status[filename] = False continue self.log.info( 'creating catalog %s based on %s', filename, self.input_file, ) with open(self.input_file, 'rb') as infile: # Although reading from the catalog template, read_po must # be fed the locale in order to correctly calculate plurals catalog = read_po(infile, locale=self.locale) catalog.locale = self._locale catalog.revision_date = datetime.datetime.now(LOCALTZ) catalog.fuzzy = False with open(filename, 'wb') as outfile: write_po(outfile, catalog) self.log.info('updating catalog %s based on %s', filename, self.input_file) with open(filename, 'rb') as infile: catalog = read_po(infile, locale=locale, domain=domain) catalog.update( template, self.no_fuzzy_matching, update_header_comment=self.update_header_comment, update_creation_date=not self.ignore_pot_creation_date, ) tmpname = os.path.join(os.path.dirname(filename), tempfile.gettempprefix() + os.path.basename(filename)) try: with open(tmpname, 'wb') as tmpfile: write_po(tmpfile, catalog, omit_header=self.omit_header, ignore_obsolete=self.ignore_obsolete, include_previous=self.previous, width=self.width) except Exception: os.remove(tmpname) raise if self.check: with open(filename, "rb") as origfile: original_catalog = read_po(origfile) with open(tmpname, "rb") as newfile: updated_catalog = read_po(newfile) updated_catalog.revision_date = original_catalog.revision_date check_status[filename] = updated_catalog.is_identical(original_catalog) os.remove(tmpname) continue try: os.rename(tmpname, filename) except OSError: # We're probably on Windows, which doesn't support atomic # renames, at least not through Python # If the error is in fact due to a permissions problem, that # same error is going to be raised from one of the following # operations os.remove(filename) shutil.copy(tmpname, filename) os.remove(tmpname) if self.check: for filename, up_to_date in check_status.items(): if up_to_date: self.log.info('Catalog %s is up to date.', filename) else: self.log.warning('Catalog %s is out of date.', filename) if not all(check_status.values()): raise BaseError("Some catalogs are out of date.") else: self.log.info("All the catalogs are up-to-date.") return class CommandLineInterface: """Command-line interface. This class provides a simple command-line interface to the message extraction and PO file generation functionality. """ usage = '%%prog %s [options] %s' version = f'%prog {VERSION}' commands = { 'compile': 'compile message catalogs to MO files', 'extract': 'extract messages from source files and generate a POT file', 'init': 'create new message catalogs from a POT file', 'update': 'update existing message catalogs from a POT file', } command_classes = { 'compile': CompileCatalog, 'extract': ExtractMessages, 'init': InitCatalog, 'update': UpdateCatalog, } log = None # Replaced on instance level def run(self, argv=None): """Main entry point of the command-line interface. :param argv: list of arguments passed on the command-line """ if argv is None: argv = sys.argv self.parser = optparse.OptionParser(usage=self.usage % ('command', '[args]'), version=self.version) self.parser.disable_interspersed_args() self.parser.print_help = self._help self.parser.add_option('--list-locales', dest='list_locales', action='store_true', help="print all known locales and exit") self.parser.add_option('-v', '--verbose', action='store_const', dest='loglevel', const=logging.DEBUG, help='print as much as possible') self.parser.add_option('-q', '--quiet', action='store_const', dest='loglevel', const=logging.ERROR, help='print as little as possible') self.parser.set_defaults(list_locales=False, loglevel=logging.INFO) options, args = self.parser.parse_args(argv[1:]) self._configure_logging(options.loglevel) if options.list_locales: identifiers = localedata.locale_identifiers() id_width = max(len(identifier) for identifier in identifiers) + 1 for identifier in sorted(identifiers): locale = Locale.parse(identifier) print(f"{identifier:<{id_width}} {locale.english_name}") return 0 if not args: self.parser.error('no valid command or option passed. ' 'Try the -h/--help option for more information.') cmdname = args[0] if cmdname not in self.commands: self.parser.error(f'unknown command "{cmdname}"') cmdinst = self._configure_command(cmdname, args[1:]) return cmdinst.run() def _configure_logging(self, loglevel): self.log = log self.log.setLevel(loglevel) # Don't add a new handler for every instance initialization (#227), this # would cause duplicated output when the CommandLineInterface as an # normal Python class. if self.log.handlers: handler = self.log.handlers[0] else: handler = logging.StreamHandler() self.log.addHandler(handler) handler.setLevel(loglevel) formatter = logging.Formatter('%(message)s') handler.setFormatter(formatter) def _help(self): print(self.parser.format_help()) print("commands:") cmd_width = max(8, max(len(command) for command in self.commands) + 1) for name, description in sorted(self.commands.items()): print(f" {name:<{cmd_width}} {description}") def _configure_command(self, cmdname, argv): """ :type cmdname: str :type argv: list[str] """ cmdclass = self.command_classes[cmdname] cmdinst = cmdclass() if self.log: cmdinst.log = self.log # Use our logger, not distutils'. assert isinstance(cmdinst, CommandMixin) cmdinst.initialize_options() parser = optparse.OptionParser( usage=self.usage % (cmdname, ''), description=self.commands[cmdname], ) as_args = getattr(cmdclass, "as_args", ()) for long, short, help in cmdclass.user_options: name = long.strip("=") default = getattr(cmdinst, name.replace("-", "_")) strs = [f"--{name}"] if short: strs.append(f"-{short}") strs.extend(cmdclass.option_aliases.get(name, ())) choices = cmdclass.option_choices.get(name, None) if name == as_args: parser.usage += f"<{name}>" elif name in cmdclass.boolean_options: parser.add_option(*strs, action="store_true", help=help) elif name in cmdclass.multiple_value_options: parser.add_option(*strs, action="append", help=help, choices=choices) else: parser.add_option(*strs, help=help, default=default, choices=choices) options, args = parser.parse_args(argv) if as_args: setattr(options, as_args.replace('-', '_'), args) for key, value in vars(options).items(): setattr(cmdinst, key, value) try: cmdinst.ensure_finalized() except OptionError as err: parser.error(str(err)) return cmdinst def main(): return CommandLineInterface().run(sys.argv) def parse_mapping(fileobj, filename=None): """Parse an extraction method mapping from a file-like object. >>> buf = StringIO(''' ... [extractors] ... custom = mypackage.module:myfunc ... ... # Python source files ... [python: **.py] ... ... # Genshi templates ... [genshi: **/templates/**.html] ... include_attrs = ... [genshi: **/templates/**.txt] ... template_class = genshi.template:TextTemplate ... encoding = latin-1 ... ... # Some custom extractor ... [custom: **/custom/*.*] ... ''') >>> method_map, options_map = parse_mapping(buf) >>> len(method_map) 4 >>> method_map[0] ('**.py', 'python') >>> options_map['**.py'] {} >>> method_map[1] ('**/templates/**.html', 'genshi') >>> options_map['**/templates/**.html']['include_attrs'] '' >>> method_map[2] ('**/templates/**.txt', 'genshi') >>> options_map['**/templates/**.txt']['template_class'] 'genshi.template:TextTemplate' >>> options_map['**/templates/**.txt']['encoding'] 'latin-1' >>> method_map[3] ('**/custom/*.*', 'mypackage.module:myfunc') >>> options_map['**/custom/*.*'] {} :param fileobj: a readable file-like object containing the configuration text to parse :see: `extract_from_directory` """ extractors = {} method_map = [] options_map = {} parser = RawConfigParser() parser._sections = OrderedDict(parser._sections) # We need ordered sections parser.read_file(fileobj, filename) for section in parser.sections(): if section == 'extractors': extractors = dict(parser.items(section)) else: method, pattern = (part.strip() for part in section.split(':', 1)) method_map.append((pattern, method)) options_map[pattern] = dict(parser.items(section)) if extractors: for idx, (pattern, method) in enumerate(method_map): if method in extractors: method = extractors[method] method_map[idx] = (pattern, method) return method_map, options_map def _parse_spec(s: str) -> tuple[int | None, tuple[int | tuple[int, str], ...]]: inds = [] number = None for x in s.split(','): if x[-1] == 't': number = int(x[:-1]) elif x[-1] == 'c': inds.append((int(x[:-1]), 'c')) else: inds.append(int(x)) return number, tuple(inds) def parse_keywords(strings: Iterable[str] = ()): """Parse keywords specifications from the given list of strings. >>> import pprint >>> keywords = ['_', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2', ... 'polymorphic:1', 'polymorphic:2,2t', 'polymorphic:3c,3t'] >>> pprint.pprint(parse_keywords(keywords)) {'_': None, 'dgettext': (2,), 'dngettext': (2, 3), 'pgettext': ((1, 'c'), 2), 'polymorphic': {None: (1,), 2: (2,), 3: ((3, 'c'),)}} The input keywords are in GNU Gettext style; see :doc:`cmdline` for details. The output is a dictionary mapping keyword names to a dictionary of specifications. Keys in this dictionary are numbers of arguments, where ``None`` means that all numbers of arguments are matched, and a number means only calls with that number of arguments are matched (which happens when using the "t" specifier). However, as a special case for backwards compatibility, if the dictionary of specifications would be ``{None: x}``, i.e., there is only one specification and it matches all argument counts, then it is collapsed into just ``x``. A specification is either a tuple or None. If a tuple, each element can be either a number ``n``, meaning that the nth argument should be extracted as a message, or the tuple ``(n, 'c')``, meaning that the nth argument should be extracted as context for the messages. A ``None`` specification is equivalent to ``(1,)``, extracting the first argument. """ keywords = {} for string in strings: if ':' in string: funcname, spec_str = string.split(':') number, spec = _parse_spec(spec_str) else: funcname = string number = None spec = None keywords.setdefault(funcname, {})[number] = spec # For best backwards compatibility, collapse {None: x} into x. for k, v in keywords.items(): if set(v) == {None}: keywords[k] = v[None] return keywords def __getattr__(name: str): # Re-exports for backwards compatibility; # `setuptools_frontend` is the canonical import location. if name in {'check_message_extractors', 'compile_catalog', 'extract_messages', 'init_catalog', 'update_catalog'}: from babel.messages import setuptools_frontend return getattr(setuptools_frontend, name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") if __name__ == '__main__': main() babel-2.14.0/babel/messages/checkers.py0000644000175000017500000001434714536056757017241 0ustar nileshnilesh""" babel.messages.checkers ~~~~~~~~~~~~~~~~~~~~~~~ Various routines that help with validation of translations. :since: version 0.9 :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations from collections.abc import Callable from babel.messages.catalog import PYTHON_FORMAT, Catalog, Message, TranslationError #: list of format chars that are compatible to each other _string_format_compatibilities = [ {'i', 'd', 'u'}, {'x', 'X'}, {'f', 'F', 'g', 'G'}, ] def num_plurals(catalog: Catalog | None, message: Message) -> None: """Verify the number of plurals in the translation.""" if not message.pluralizable: if not isinstance(message.string, str): raise TranslationError("Found plural forms for non-pluralizable " "message") return # skip further tests if no catalog is provided. elif catalog is None: return msgstrs = message.string if not isinstance(msgstrs, (list, tuple)): msgstrs = (msgstrs,) if len(msgstrs) != catalog.num_plurals: raise TranslationError("Wrong number of plural forms (expected %d)" % catalog.num_plurals) def python_format(catalog: Catalog | None, message: Message) -> None: """Verify the format string placeholders in the translation.""" if 'python-format' not in message.flags: return msgids = message.id if not isinstance(msgids, (list, tuple)): msgids = (msgids,) msgstrs = message.string if not isinstance(msgstrs, (list, tuple)): msgstrs = (msgstrs,) for msgid, msgstr in zip(msgids, msgstrs): if msgstr: _validate_format(msgid, msgstr) def _validate_format(format: str, alternative: str) -> None: """Test format string `alternative` against `format`. `format` can be the msgid of a message and `alternative` one of the `msgstr`\\s. The two arguments are not interchangeable as `alternative` may contain less placeholders if `format` uses named placeholders. The behavior of this function is undefined if the string does not use string formatting. If the string formatting of `alternative` is compatible to `format` the function returns `None`, otherwise a `TranslationError` is raised. Examples for compatible format strings: >>> _validate_format('Hello %s!', 'Hallo %s!') >>> _validate_format('Hello %i!', 'Hallo %d!') Example for an incompatible format strings: >>> _validate_format('Hello %(name)s!', 'Hallo %s!') Traceback (most recent call last): ... TranslationError: the format strings are of different kinds This function is used by the `python_format` checker. :param format: The original format string :param alternative: The alternative format string that should be checked against format :raises TranslationError: on formatting errors """ def _parse(string: str) -> list[tuple[str, str]]: result: list[tuple[str, str]] = [] for match in PYTHON_FORMAT.finditer(string): name, format, typechar = match.groups() if typechar == '%' and name is None: continue result.append((name, str(typechar))) return result def _compatible(a: str, b: str) -> bool: if a == b: return True for set in _string_format_compatibilities: if a in set and b in set: return True return False def _check_positional(results: list[tuple[str, str]]) -> bool: positional = None for name, _char in results: if positional is None: positional = name is None else: if (name is None) != positional: raise TranslationError('format string mixes positional ' 'and named placeholders') return bool(positional) a, b = map(_parse, (format, alternative)) # now check if both strings are positional or named a_positional, b_positional = map(_check_positional, (a, b)) if a_positional and not b_positional and not b: raise TranslationError('placeholders are incompatible') elif a_positional != b_positional: raise TranslationError('the format strings are of different kinds') # if we are operating on positional strings both must have the # same number of format chars and those must be compatible if a_positional: if len(a) != len(b): raise TranslationError('positional format placeholders are ' 'unbalanced') for idx, ((_, first), (_, second)) in enumerate(zip(a, b)): if not _compatible(first, second): raise TranslationError('incompatible format for placeholder ' '%d: %r and %r are not compatible' % (idx + 1, first, second)) # otherwise the second string must not have names the first one # doesn't have and the types of those included must be compatible else: type_map = dict(a) for name, typechar in b: if name not in type_map: raise TranslationError(f'unknown named placeholder {name!r}') elif not _compatible(typechar, type_map[name]): raise TranslationError( f'incompatible format for placeholder {name!r}: ' f'{typechar!r} and {type_map[name]!r} are not compatible', ) def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]: checkers: list[Callable[[Catalog | None, Message], object]] = [] try: from pkg_resources import working_set except ImportError: pass else: for entry_point in working_set.iter_entry_points('babel.checkers'): checkers.append(entry_point.load()) if len(checkers) == 0: # if pkg_resources is not available or no usable egg-info was found # (see #230), just resort to hard-coded checkers return [num_plurals, python_format] return checkers checkers: list[Callable[[Catalog | None, Message], object]] = _find_checkers() babel-2.14.0/babel/messages/pofile.py0000644000175000017500000005346514536056757016734 0ustar nileshnilesh""" babel.messages.pofile ~~~~~~~~~~~~~~~~~~~~~ Reading and writing of files in the ``gettext`` PO (portable object) format. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import os import re from collections.abc import Iterable from typing import TYPE_CHECKING from babel.core import Locale from babel.messages.catalog import Catalog, Message from babel.util import _cmp, wraptext if TYPE_CHECKING: from typing import IO, AnyStr from _typeshed import SupportsWrite from typing_extensions import Literal def unescape(string: str) -> str: r"""Reverse `escape` the given string. >>> print(unescape('"Say:\\n \\"hello, world!\\"\\n"')) Say: "hello, world!" :param string: the string to unescape """ def replace_escapes(match): m = match.group(1) if m == 'n': return '\n' elif m == 't': return '\t' elif m == 'r': return '\r' # m is \ or " return m return re.compile(r'\\([\\trn"])').sub(replace_escapes, string[1:-1]) def denormalize(string: str) -> str: r"""Reverse the normalization done by the `normalize` function. >>> print(denormalize(r'''"" ... "Say:\n" ... " \"hello, world!\"\n"''')) Say: "hello, world!" >>> print(denormalize(r'''"" ... "Say:\n" ... " \"Lorem ipsum dolor sit " ... "amet, consectetur adipisicing" ... " elit, \"\n"''')) Say: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " :param string: the string to denormalize """ if '\n' in string: escaped_lines = string.splitlines() if string.startswith('""'): escaped_lines = escaped_lines[1:] lines = map(unescape, escaped_lines) return ''.join(lines) else: return unescape(string) class PoFileError(Exception): """Exception thrown by PoParser when an invalid po file is encountered.""" def __init__(self, message: str, catalog: Catalog, line: str, lineno: int) -> None: super().__init__(f'{message} on {lineno}') self.catalog = catalog self.line = line self.lineno = lineno class _NormalizedString: def __init__(self, *args: str) -> None: self._strs: list[str] = [] for arg in args: self.append(arg) def append(self, s: str) -> None: self._strs.append(s.strip()) def denormalize(self) -> str: return ''.join(map(unescape, self._strs)) def __bool__(self) -> bool: return bool(self._strs) def __repr__(self) -> str: return os.linesep.join(self._strs) def __cmp__(self, other: object) -> int: if not other: return 1 return _cmp(str(self), str(other)) def __gt__(self, other: object) -> bool: return self.__cmp__(other) > 0 def __lt__(self, other: object) -> bool: return self.__cmp__(other) < 0 def __ge__(self, other: object) -> bool: return self.__cmp__(other) >= 0 def __le__(self, other: object) -> bool: return self.__cmp__(other) <= 0 def __eq__(self, other: object) -> bool: return self.__cmp__(other) == 0 def __ne__(self, other: object) -> bool: return self.__cmp__(other) != 0 class PoFileParser: """Support class to read messages from a ``gettext`` PO (portable object) file and add them to a `Catalog` See `read_po` for simple cases. """ _keywords = [ 'msgid', 'msgstr', 'msgctxt', 'msgid_plural', ] def __init__(self, catalog: Catalog, ignore_obsolete: bool = False, abort_invalid: bool = False) -> None: self.catalog = catalog self.ignore_obsolete = ignore_obsolete self.counter = 0 self.offset = 0 self.abort_invalid = abort_invalid self._reset_message_state() def _reset_message_state(self) -> None: self.messages = [] self.translations = [] self.locations = [] self.flags = [] self.user_comments = [] self.auto_comments = [] self.context = None self.obsolete = False self.in_msgid = False self.in_msgstr = False self.in_msgctxt = False def _add_message(self) -> None: """ Add a message to the catalog based on the current parser state and clear the state ready to process the next message. """ self.translations.sort() if len(self.messages) > 1: msgid = tuple(m.denormalize() for m in self.messages) else: msgid = self.messages[0].denormalize() if isinstance(msgid, (list, tuple)): string = ['' for _ in range(self.catalog.num_plurals)] for idx, translation in self.translations: if idx >= self.catalog.num_plurals: self._invalid_pofile("", self.offset, "msg has more translations than num_plurals of catalog") continue string[idx] = translation.denormalize() string = tuple(string) else: string = self.translations[0][1].denormalize() msgctxt = self.context.denormalize() if self.context else None message = Message(msgid, string, list(self.locations), set(self.flags), self.auto_comments, self.user_comments, lineno=self.offset + 1, context=msgctxt) if self.obsolete: if not self.ignore_obsolete: self.catalog.obsolete[msgid] = message else: self.catalog[msgid] = message self.counter += 1 self._reset_message_state() def _finish_current_message(self) -> None: if self.messages: self._add_message() def _process_message_line(self, lineno, line, obsolete=False) -> None: if line.startswith('"'): self._process_string_continuation_line(line, lineno) else: self._process_keyword_line(lineno, line, obsolete) def _process_keyword_line(self, lineno, line, obsolete=False) -> None: for keyword in self._keywords: try: if line.startswith(keyword) and line[len(keyword)] in [' ', '[']: arg = line[len(keyword):] break except IndexError: self._invalid_pofile(line, lineno, "Keyword must be followed by a string") else: self._invalid_pofile(line, lineno, "Start of line didn't match any expected keyword.") return if keyword in ['msgid', 'msgctxt']: self._finish_current_message() self.obsolete = obsolete # The line that has the msgid is stored as the offset of the msg # should this be the msgctxt if it has one? if keyword == 'msgid': self.offset = lineno if keyword in ['msgid', 'msgid_plural']: self.in_msgctxt = False self.in_msgid = True self.messages.append(_NormalizedString(arg)) elif keyword == 'msgstr': self.in_msgid = False self.in_msgstr = True if arg.startswith('['): idx, msg = arg[1:].split(']', 1) self.translations.append([int(idx), _NormalizedString(msg)]) else: self.translations.append([0, _NormalizedString(arg)]) elif keyword == 'msgctxt': self.in_msgctxt = True self.context = _NormalizedString(arg) def _process_string_continuation_line(self, line, lineno) -> None: if self.in_msgid: s = self.messages[-1] elif self.in_msgstr: s = self.translations[-1][1] elif self.in_msgctxt: s = self.context else: self._invalid_pofile(line, lineno, "Got line starting with \" but not in msgid, msgstr or msgctxt") return s.append(line) def _process_comment(self, line) -> None: self._finish_current_message() if line[1:].startswith(':'): for location in line[2:].lstrip().split(): pos = location.rfind(':') if pos >= 0: try: lineno = int(location[pos + 1:]) except ValueError: continue self.locations.append((location[:pos], lineno)) else: self.locations.append((location, None)) elif line[1:].startswith(','): for flag in line[2:].lstrip().split(','): self.flags.append(flag.strip()) elif line[1:].startswith('.'): # These are called auto-comments comment = line[2:].strip() if comment: # Just check that we're not adding empty comments self.auto_comments.append(comment) else: # These are called user comments self.user_comments.append(line[1:].strip()) def parse(self, fileobj: IO[AnyStr]) -> None: """ Reads from the file-like object `fileobj` and adds any po file units found in it to the `Catalog` supplied to the constructor. """ for lineno, line in enumerate(fileobj): line = line.strip() if not isinstance(line, str): line = line.decode(self.catalog.charset) if not line: continue if line.startswith('#'): if line[1:].startswith('~'): self._process_message_line(lineno, line[2:].lstrip(), obsolete=True) else: self._process_comment(line) else: self._process_message_line(lineno, line) self._finish_current_message() # No actual messages found, but there was some info in comments, from which # we'll construct an empty header message if not self.counter and (self.flags or self.user_comments or self.auto_comments): self.messages.append(_NormalizedString('""')) self.translations.append([0, _NormalizedString('""')]) self._add_message() def _invalid_pofile(self, line, lineno, msg) -> None: assert isinstance(line, str) if self.abort_invalid: raise PoFileError(msg, self.catalog, line, lineno) print("WARNING:", msg) print(f"WARNING: Problem on line {lineno + 1}: {line!r}") def read_po( fileobj: IO[AnyStr], locale: str | Locale | None = None, domain: str | None = None, ignore_obsolete: bool = False, charset: str | None = None, abort_invalid: bool = False, ) -> Catalog: """Read messages from a ``gettext`` PO (portable object) file from the given file-like object and return a `Catalog`. >>> from datetime import datetime >>> from io import StringIO >>> buf = StringIO(''' ... #: main.py:1 ... #, fuzzy, python-format ... msgid "foo %(name)s" ... msgstr "quux %(name)s" ... ... # A user comment ... #. An auto comment ... #: main.py:3 ... msgid "bar" ... msgid_plural "baz" ... msgstr[0] "bar" ... msgstr[1] "baaz" ... ''') >>> catalog = read_po(buf) >>> catalog.revision_date = datetime(2007, 4, 1) >>> for message in catalog: ... if message.id: ... print((message.id, message.string)) ... print(' ', (message.locations, sorted(list(message.flags)))) ... print(' ', (message.user_comments, message.auto_comments)) (u'foo %(name)s', u'quux %(name)s') ([(u'main.py', 1)], [u'fuzzy', u'python-format']) ([], []) ((u'bar', u'baz'), (u'bar', u'baaz')) ([(u'main.py', 3)], []) ([u'A user comment'], [u'An auto comment']) .. versionadded:: 1.0 Added support for explicit charset argument. :param fileobj: the file-like object to read the PO file from :param locale: the locale identifier or `Locale` object, or `None` if the catalog is not bound to a locale (which basically means it's a template) :param domain: the message domain :param ignore_obsolete: whether to ignore obsolete messages in the input :param charset: the character set of the catalog. :param abort_invalid: abort read if po file is invalid """ catalog = Catalog(locale=locale, domain=domain, charset=charset) parser = PoFileParser(catalog, ignore_obsolete, abort_invalid=abort_invalid) parser.parse(fileobj) return catalog WORD_SEP = re.compile('(' r'\s+|' # any whitespace r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)' # em-dash ')') def escape(string: str) -> str: r"""Escape the given string so that it can be included in double-quoted strings in ``PO`` files. >>> escape('''Say: ... "hello, world!" ... ''') '"Say:\\n \\"hello, world!\\"\\n"' :param string: the string to escape """ return '"%s"' % string.replace('\\', '\\\\') \ .replace('\t', '\\t') \ .replace('\r', '\\r') \ .replace('\n', '\\n') \ .replace('\"', '\\"') def normalize(string: str, prefix: str = '', width: int = 76) -> str: r"""Convert a string into a format that is appropriate for .po files. >>> print(normalize('''Say: ... "hello, world!" ... ''', width=None)) "" "Say:\n" " \"hello, world!\"\n" >>> print(normalize('''Say: ... "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " ... ''', width=32)) "" "Say:\n" " \"Lorem ipsum dolor sit " "amet, consectetur adipisicing" " elit, \"\n" :param string: the string to normalize :param prefix: a string that should be prepended to every line :param width: the maximum line width; use `None`, 0, or a negative number to completely disable line wrapping """ if width and width > 0: prefixlen = len(prefix) lines = [] for line in string.splitlines(True): if len(escape(line)) + prefixlen > width: chunks = WORD_SEP.split(line) chunks.reverse() while chunks: buf = [] size = 2 while chunks: length = len(escape(chunks[-1])) - 2 + prefixlen if size + length < width: buf.append(chunks.pop()) size += length else: if not buf: # handle long chunks by putting them on a # separate line buf.append(chunks.pop()) break lines.append(''.join(buf)) else: lines.append(line) else: lines = string.splitlines(True) if len(lines) <= 1: return escape(string) # Remove empty trailing line if lines and not lines[-1]: del lines[-1] lines[-1] += '\n' return '""\n' + '\n'.join([(prefix + escape(line)) for line in lines]) def write_po( fileobj: SupportsWrite[bytes], catalog: Catalog, width: int = 76, no_location: bool = False, omit_header: bool = False, sort_output: bool = False, sort_by_file: bool = False, ignore_obsolete: bool = False, include_previous: bool = False, include_lineno: bool = True, ) -> None: r"""Write a ``gettext`` PO (portable object) template file for a given message catalog to the provided file-like object. >>> catalog = Catalog() >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)], ... flags=('fuzzy',)) >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)]) >>> from io import BytesIO >>> buf = BytesIO() >>> write_po(buf, catalog, omit_header=True) >>> print(buf.getvalue().decode("utf8")) #: main.py:1 #, fuzzy, python-format msgid "foo %(name)s" msgstr "" #: main.py:3 msgid "bar" msgid_plural "baz" msgstr[0] "" msgstr[1] "" :param fileobj: the file-like object to write to :param catalog: the `Catalog` instance :param width: the maximum line width for the generated output; use `None`, 0, or a negative number to completely disable line wrapping :param no_location: do not emit a location comment for every message :param omit_header: do not include the ``msgid ""`` entry at the top of the output :param sort_output: whether to sort the messages in the output by msgid :param sort_by_file: whether to sort the messages in the output by their locations :param ignore_obsolete: whether to ignore obsolete messages and not include them in the output; by default they are included as comments :param include_previous: include the old msgid as a comment when updating the catalog :param include_lineno: include line number in the location comment """ def _normalize(key, prefix=''): return normalize(key, prefix=prefix, width=width) def _write(text): if isinstance(text, str): text = text.encode(catalog.charset, 'backslashreplace') fileobj.write(text) def _write_comment(comment, prefix=''): # xgettext always wraps comments even if --no-wrap is passed; # provide the same behaviour _width = width if width and width > 0 else 76 for line in wraptext(comment, _width): _write(f"#{prefix} {line.strip()}\n") def _write_message(message, prefix=''): if isinstance(message.id, (list, tuple)): if message.context: _write(f"{prefix}msgctxt {_normalize(message.context, prefix)}\n") _write(f"{prefix}msgid {_normalize(message.id[0], prefix)}\n") _write(f"{prefix}msgid_plural {_normalize(message.id[1], prefix)}\n") for idx in range(catalog.num_plurals): try: string = message.string[idx] except IndexError: string = '' _write(f"{prefix}msgstr[{idx:d}] {_normalize(string, prefix)}\n") else: if message.context: _write(f"{prefix}msgctxt {_normalize(message.context, prefix)}\n") _write(f"{prefix}msgid {_normalize(message.id, prefix)}\n") _write(f"{prefix}msgstr {_normalize(message.string or '', prefix)}\n") sort_by = None if sort_output: sort_by = "message" elif sort_by_file: sort_by = "location" for message in _sort_messages(catalog, sort_by=sort_by): if not message.id: # This is the header "message" if omit_header: continue comment_header = catalog.header_comment if width and width > 0: lines = [] for line in comment_header.splitlines(): lines += wraptext(line, width=width, subsequent_indent='# ') comment_header = '\n'.join(lines) _write(f"{comment_header}\n") for comment in message.user_comments: _write_comment(comment) for comment in message.auto_comments: _write_comment(comment, prefix='.') if not no_location: locs = [] # sort locations by filename and lineno. # if there's no as lineno, use `-1`. # if no sorting possible, leave unsorted. # (see issue #606) try: locations = sorted(message.locations, key=lambda x: (x[0], isinstance(x[1], int) and x[1] or -1)) except TypeError: # e.g. "TypeError: unorderable types: NoneType() < int()" locations = message.locations for filename, lineno in locations: location = filename.replace(os.sep, '/') if lineno and include_lineno: location = f"{location}:{lineno:d}" if location not in locs: locs.append(location) _write_comment(' '.join(locs), prefix=':') if message.flags: _write(f"#{', '.join(['', *sorted(message.flags)])}\n") if message.previous_id and include_previous: _write_comment( f'msgid {_normalize(message.previous_id[0])}', prefix='|', ) if len(message.previous_id) > 1: _write_comment('msgid_plural %s' % _normalize( message.previous_id[1], ), prefix='|') _write_message(message) _write('\n') if not ignore_obsolete: for message in _sort_messages( catalog.obsolete.values(), sort_by=sort_by, ): for comment in message.user_comments: _write_comment(comment) _write_message(message, prefix='#~ ') _write('\n') def _sort_messages(messages: Iterable[Message], sort_by: Literal["message", "location"]) -> list[Message]: """ Sort the given message iterable by the given criteria. Always returns a list. :param messages: An iterable of Messages. :param sort_by: Sort by which criteria? Options are `message` and `location`. :return: list[Message] """ messages = list(messages) if sort_by == "message": messages.sort() elif sort_by == "location": messages.sort(key=lambda m: m.locations) return messages babel-2.14.0/babel/messages/extract.py0000644000175000017500000010303514536056757017115 0ustar nileshnilesh""" babel.messages.extract ~~~~~~~~~~~~~~~~~~~~~~ Basic infrastructure for extracting localizable messages from source files. This module defines an extensible system for collecting localizable message strings from a variety of sources. A native extractor for Python source files is builtin, extractors for other sources can be added using very simple plugins. The main entry points into the extraction functionality are the functions `extract_from_dir` and `extract_from_file`. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import ast import io import os import sys import tokenize from collections.abc import ( Callable, Collection, Generator, Iterable, Mapping, MutableSequence, ) from os.path import relpath from textwrap import dedent from tokenize import COMMENT, NAME, OP, STRING, generate_tokens from typing import TYPE_CHECKING, Any from babel.util import parse_encoding, parse_future_flags, pathmatch if TYPE_CHECKING: from typing import IO, Protocol from _typeshed import SupportsItems, SupportsRead, SupportsReadline from typing_extensions import Final, TypeAlias, TypedDict class _PyOptions(TypedDict, total=False): encoding: str class _JSOptions(TypedDict, total=False): encoding: str jsx: bool template_string: bool parse_template_string: bool class _FileObj(SupportsRead[bytes], SupportsReadline[bytes], Protocol): def seek(self, __offset: int, __whence: int = ...) -> int: ... def tell(self) -> int: ... _SimpleKeyword: TypeAlias = tuple[int | tuple[int, int] | tuple[int, str], ...] | None _Keyword: TypeAlias = dict[int | None, _SimpleKeyword] | _SimpleKeyword # 5-tuple of (filename, lineno, messages, comments, context) _FileExtractionResult: TypeAlias = tuple[str, int, str | tuple[str, ...], list[str], str | None] # 4-tuple of (lineno, message, comments, context) _ExtractionResult: TypeAlias = tuple[int, str | tuple[str, ...], list[str], str | None] # Required arguments: fileobj, keywords, comment_tags, options # Return value: Iterable of (lineno, message, comments, context) _CallableExtractionMethod: TypeAlias = Callable[ [_FileObj | IO[bytes], Mapping[str, _Keyword], Collection[str], Mapping[str, Any]], Iterable[_ExtractionResult], ] _ExtractionMethod: TypeAlias = _CallableExtractionMethod | str GROUP_NAME: Final[str] = 'babel.extractors' DEFAULT_KEYWORDS: dict[str, _Keyword] = { '_': None, 'gettext': None, 'ngettext': (1, 2), 'ugettext': None, 'ungettext': (1, 2), 'dgettext': (2,), 'dngettext': (2, 3), 'N_': None, 'pgettext': ((1, 'c'), 2), 'npgettext': ((1, 'c'), 2, 3), } DEFAULT_MAPPING: list[tuple[str, str]] = [('**.py', 'python')] # New tokens in Python 3.12, or None on older versions FSTRING_START = getattr(tokenize, "FSTRING_START", None) FSTRING_MIDDLE = getattr(tokenize, "FSTRING_MIDDLE", None) FSTRING_END = getattr(tokenize, "FSTRING_END", None) def _strip_comment_tags(comments: MutableSequence[str], tags: Iterable[str]): """Helper function for `extract` that strips comment tags from strings in a list of comment lines. This functions operates in-place. """ def _strip(line: str): for tag in tags: if line.startswith(tag): return line[len(tag):].strip() return line comments[:] = map(_strip, comments) def default_directory_filter(dirpath: str | os.PathLike[str]) -> bool: subdir = os.path.basename(dirpath) # Legacy default behavior: ignore dot and underscore directories return not (subdir.startswith('.') or subdir.startswith('_')) def extract_from_dir( dirname: str | os.PathLike[str] | None = None, method_map: Iterable[tuple[str, str]] = DEFAULT_MAPPING, options_map: SupportsItems[str, dict[str, Any]] | None = None, keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS, comment_tags: Collection[str] = (), callback: Callable[[str, str, dict[str, Any]], object] | None = None, strip_comment_tags: bool = False, directory_filter: Callable[[str], bool] | None = None, ) -> Generator[_FileExtractionResult, None, None]: """Extract messages from any source files found in the given directory. This function generates tuples of the form ``(filename, lineno, message, comments, context)``. Which extraction method is used per file is determined by the `method_map` parameter, which maps extended glob patterns to extraction method names. For example, the following is the default mapping: >>> method_map = [ ... ('**.py', 'python') ... ] This basically says that files with the filename extension ".py" at any level inside the directory should be processed by the "python" extraction method. Files that don't match any of the mapping patterns are ignored. See the documentation of the `pathmatch` function for details on the pattern syntax. The following extended mapping would also use the "genshi" extraction method on any file in "templates" subdirectory: >>> method_map = [ ... ('**/templates/**.*', 'genshi'), ... ('**.py', 'python') ... ] The dictionary provided by the optional `options_map` parameter augments these mappings. It uses extended glob patterns as keys, and the values are dictionaries mapping options names to option values (both strings). The glob patterns of the `options_map` do not necessarily need to be the same as those used in the method mapping. For example, while all files in the ``templates`` folders in an application may be Genshi applications, the options for those files may differ based on extension: >>> options_map = { ... '**/templates/**.txt': { ... 'template_class': 'genshi.template:TextTemplate', ... 'encoding': 'latin-1' ... }, ... '**/templates/**.html': { ... 'include_attrs': '' ... } ... } :param dirname: the path to the directory to extract messages from. If not given the current working directory is used. :param method_map: a list of ``(pattern, method)`` tuples that maps of extraction method names to extended glob patterns :param options_map: a dictionary of additional options (optional) :param keywords: a dictionary mapping keywords (i.e. names of functions that should be recognized as translation functions) to tuples that specify which of their arguments contain localizable strings :param comment_tags: a list of tags of translator comments to search for and include in the results :param callback: a function that is called for every file that message are extracted from, just before the extraction itself is performed; the function is passed the filename, the name of the extraction method and and the options dictionary as positional arguments, in that order :param strip_comment_tags: a flag that if set to `True` causes all comment tags to be removed from the collected comments. :param directory_filter: a callback to determine whether a directory should be recursed into. Receives the full directory path; should return True if the directory is valid. :see: `pathmatch` """ if dirname is None: dirname = os.getcwd() if options_map is None: options_map = {} if directory_filter is None: directory_filter = default_directory_filter absname = os.path.abspath(dirname) for root, dirnames, filenames in os.walk(absname): dirnames[:] = [ subdir for subdir in dirnames if directory_filter(os.path.join(root, subdir)) ] dirnames.sort() filenames.sort() for filename in filenames: filepath = os.path.join(root, filename).replace(os.sep, '/') yield from check_and_call_extract_file( filepath, method_map, options_map, callback, keywords, comment_tags, strip_comment_tags, dirpath=absname, ) def check_and_call_extract_file( filepath: str | os.PathLike[str], method_map: Iterable[tuple[str, str]], options_map: SupportsItems[str, dict[str, Any]], callback: Callable[[str, str, dict[str, Any]], object] | None, keywords: Mapping[str, _Keyword], comment_tags: Collection[str], strip_comment_tags: bool, dirpath: str | os.PathLike[str] | None = None, ) -> Generator[_FileExtractionResult, None, None]: """Checks if the given file matches an extraction method mapping, and if so, calls extract_from_file. Note that the extraction method mappings are based relative to dirpath. So, given an absolute path to a file `filepath`, we want to check using just the relative path from `dirpath` to `filepath`. Yields 5-tuples (filename, lineno, messages, comments, context). :param filepath: An absolute path to a file that exists. :param method_map: a list of ``(pattern, method)`` tuples that maps of extraction method names to extended glob patterns :param options_map: a dictionary of additional options (optional) :param callback: a function that is called for every file that message are extracted from, just before the extraction itself is performed; the function is passed the filename, the name of the extraction method and and the options dictionary as positional arguments, in that order :param keywords: a dictionary mapping keywords (i.e. names of functions that should be recognized as translation functions) to tuples that specify which of their arguments contain localizable strings :param comment_tags: a list of tags of translator comments to search for and include in the results :param strip_comment_tags: a flag that if set to `True` causes all comment tags to be removed from the collected comments. :param dirpath: the path to the directory to extract messages from. :return: iterable of 5-tuples (filename, lineno, messages, comments, context) :rtype: Iterable[tuple[str, int, str|tuple[str], list[str], str|None] """ # filename is the relative path from dirpath to the actual file filename = relpath(filepath, dirpath) for pattern, method in method_map: if not pathmatch(pattern, filename): continue options = {} for opattern, odict in options_map.items(): if pathmatch(opattern, filename): options = odict if callback: callback(filename, method, options) for message_tuple in extract_from_file( method, filepath, keywords=keywords, comment_tags=comment_tags, options=options, strip_comment_tags=strip_comment_tags, ): yield (filename, *message_tuple) break def extract_from_file( method: _ExtractionMethod, filename: str | os.PathLike[str], keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS, comment_tags: Collection[str] = (), options: Mapping[str, Any] | None = None, strip_comment_tags: bool = False, ) -> list[_ExtractionResult]: """Extract messages from a specific file. This function returns a list of tuples of the form ``(lineno, message, comments, context)``. :param filename: the path to the file to extract messages from :param method: a string specifying the extraction method (.e.g. "python") :param keywords: a dictionary mapping keywords (i.e. names of functions that should be recognized as translation functions) to tuples that specify which of their arguments contain localizable strings :param comment_tags: a list of translator tags to search for and include in the results :param strip_comment_tags: a flag that if set to `True` causes all comment tags to be removed from the collected comments. :param options: a dictionary of additional options (optional) :returns: list of tuples of the form ``(lineno, message, comments, context)`` :rtype: list[tuple[int, str|tuple[str], list[str], str|None] """ if method == 'ignore': return [] with open(filename, 'rb') as fileobj: return list(extract(method, fileobj, keywords, comment_tags, options, strip_comment_tags)) def _match_messages_against_spec(lineno: int, messages: list[str|None], comments: list[str], fileobj: _FileObj, spec: tuple[int|tuple[int, str], ...]): translatable = [] context = None # last_index is 1 based like the keyword spec last_index = len(messages) for index in spec: if isinstance(index, tuple): # (n, 'c') context = messages[index[0] - 1] continue if last_index < index: # Not enough arguments return message = messages[index - 1] if message is None: return translatable.append(message) # keyword spec indexes are 1 based, therefore '-1' if isinstance(spec[0], tuple): # context-aware *gettext method first_msg_index = spec[1] - 1 else: first_msg_index = spec[0] - 1 # An empty string msgid isn't valid, emit a warning if not messages[first_msg_index]: filename = (getattr(fileobj, "name", None) or "(unknown)") sys.stderr.write( f"{filename}:{lineno}: warning: Empty msgid. It is reserved by GNU gettext: gettext(\"\") " f"returns the header entry with meta information, not the empty string.\n", ) return translatable = tuple(translatable) if len(translatable) == 1: translatable = translatable[0] return lineno, translatable, comments, context def extract( method: _ExtractionMethod, fileobj: _FileObj, keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS, comment_tags: Collection[str] = (), options: Mapping[str, Any] | None = None, strip_comment_tags: bool = False, ) -> Generator[_ExtractionResult, None, None]: """Extract messages from the given file-like object using the specified extraction method. This function returns tuples of the form ``(lineno, message, comments, context)``. The implementation dispatches the actual extraction to plugins, based on the value of the ``method`` parameter. >>> source = b'''# foo module ... def run(argv): ... print(_('Hello, world!')) ... ''' >>> from io import BytesIO >>> for message in extract('python', BytesIO(source)): ... print(message) (3, u'Hello, world!', [], None) :param method: an extraction method (a callable), or a string specifying the extraction method (.e.g. "python"); if this is a simple name, the extraction function will be looked up by entry point; if it is an explicit reference to a function (of the form ``package.module:funcname`` or ``package.module.funcname``), the corresponding function will be imported and used :param fileobj: the file-like object the messages should be extracted from :param keywords: a dictionary mapping keywords (i.e. names of functions that should be recognized as translation functions) to tuples that specify which of their arguments contain localizable strings :param comment_tags: a list of translator tags to search for and include in the results :param options: a dictionary of additional options (optional) :param strip_comment_tags: a flag that if set to `True` causes all comment tags to be removed from the collected comments. :raise ValueError: if the extraction method is not registered :returns: iterable of tuples of the form ``(lineno, message, comments, context)`` :rtype: Iterable[tuple[int, str|tuple[str], list[str], str|None] """ func = None if callable(method): func = method elif ':' in method or '.' in method: if ':' not in method: lastdot = method.rfind('.') module, attrname = method[:lastdot], method[lastdot + 1:] else: module, attrname = method.split(':', 1) func = getattr(__import__(module, {}, {}, [attrname]), attrname) else: try: from pkg_resources import working_set except ImportError: pass else: for entry_point in working_set.iter_entry_points(GROUP_NAME, method): func = entry_point.load(require=True) break if func is None: # if pkg_resources is not available or no usable egg-info was found # (see #230), we resort to looking up the builtin extractors # directly builtin = { 'ignore': extract_nothing, 'python': extract_python, 'javascript': extract_javascript, } func = builtin.get(method) if func is None: raise ValueError(f"Unknown extraction method {method!r}") results = func(fileobj, keywords.keys(), comment_tags, options=options or {}) for lineno, funcname, messages, comments in results: if not isinstance(messages, (list, tuple)): messages = [messages] if not messages: continue specs = keywords[funcname] or None if funcname else None # {None: x} may be collapsed into x for backwards compatibility. if not isinstance(specs, dict): specs = {None: specs} if strip_comment_tags: _strip_comment_tags(comments, comment_tags) # None matches all arities. for arity in (None, len(messages)): try: spec = specs[arity] except KeyError: continue if spec is None: spec = (1,) result = _match_messages_against_spec(lineno, messages, comments, fileobj, spec) if result is not None: yield result def extract_nothing( fileobj: _FileObj, keywords: Mapping[str, _Keyword], comment_tags: Collection[str], options: Mapping[str, Any], ) -> list[_ExtractionResult]: """Pseudo extractor that does not actually extract anything, but simply returns an empty list. """ return [] def extract_python( fileobj: IO[bytes], keywords: Mapping[str, _Keyword], comment_tags: Collection[str], options: _PyOptions, ) -> Generator[_ExtractionResult, None, None]: """Extract messages from Python source code. It returns an iterator yielding tuples in the following form ``(lineno, funcname, message, comments)``. :param fileobj: the seekable, file-like object the messages should be extracted from :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions :param comment_tags: a list of translator tags to search for and include in the results :param options: a dictionary of additional options (optional) :rtype: ``iterator`` """ funcname = lineno = message_lineno = None call_stack = -1 buf = [] messages = [] translator_comments = [] in_def = in_translator_comments = False comment_tag = None encoding = parse_encoding(fileobj) or options.get('encoding', 'UTF-8') future_flags = parse_future_flags(fileobj, encoding) next_line = lambda: fileobj.readline().decode(encoding) tokens = generate_tokens(next_line) # Current prefix of a Python 3.12 (PEP 701) f-string, or None if we're not # currently parsing one. current_fstring_start = None for tok, value, (lineno, _), _, _ in tokens: if call_stack == -1 and tok == NAME and value in ('def', 'class'): in_def = True elif tok == OP and value == '(': if in_def: # Avoid false positives for declarations such as: # def gettext(arg='message'): in_def = False continue if funcname: message_lineno = lineno call_stack += 1 elif in_def and tok == OP and value == ':': # End of a class definition without parens in_def = False continue elif call_stack == -1 and tok == COMMENT: # Strip the comment token from the line value = value[1:].strip() if in_translator_comments and \ translator_comments[-1][0] == lineno - 1: # We're already inside a translator comment, continue appending translator_comments.append((lineno, value)) continue # If execution reaches this point, let's see if comment line # starts with one of the comment tags for comment_tag in comment_tags: if value.startswith(comment_tag): in_translator_comments = True translator_comments.append((lineno, value)) break elif funcname and call_stack == 0: nested = (tok == NAME and value in keywords) if (tok == OP and value == ')') or nested: if buf: messages.append(''.join(buf)) del buf[:] else: messages.append(None) messages = tuple(messages) if len(messages) > 1 else messages[0] # Comments don't apply unless they immediately # precede the message if translator_comments and \ translator_comments[-1][0] < message_lineno - 1: translator_comments = [] yield (message_lineno, funcname, messages, [comment[1] for comment in translator_comments]) funcname = lineno = message_lineno = None call_stack = -1 messages = [] translator_comments = [] in_translator_comments = False if nested: funcname = value elif tok == STRING: val = _parse_python_string(value, encoding, future_flags) if val is not None: buf.append(val) # Python 3.12+, see https://peps.python.org/pep-0701/#new-tokens elif tok == FSTRING_START: current_fstring_start = value elif tok == FSTRING_MIDDLE: if current_fstring_start is not None: current_fstring_start += value elif tok == FSTRING_END: if current_fstring_start is not None: fstring = current_fstring_start + value val = _parse_python_string(fstring, encoding, future_flags) if val is not None: buf.append(val) elif tok == OP and value == ',': if buf: messages.append(''.join(buf)) del buf[:] else: messages.append(None) if translator_comments: # We have translator comments, and since we're on a # comma(,) user is allowed to break into a new line # Let's increase the last comment's lineno in order # for the comment to still be a valid one old_lineno, old_comment = translator_comments.pop() translator_comments.append((old_lineno + 1, old_comment)) elif call_stack > 0 and tok == OP and value == ')': call_stack -= 1 elif funcname and call_stack == -1: funcname = None elif tok == NAME and value in keywords: funcname = value if (current_fstring_start is not None and tok not in {FSTRING_START, FSTRING_MIDDLE} ): # In Python 3.12, tokens other than FSTRING_* mean the # f-string is dynamic, so we don't wan't to extract it. # And if it's FSTRING_END, we've already handled it above. # Let's forget that we're in an f-string. current_fstring_start = None def _parse_python_string(value: str, encoding: str, future_flags: int) -> str | None: # Unwrap quotes in a safe manner, maintaining the string's encoding # https://sourceforge.net/tracker/?func=detail&atid=355470&aid=617979&group_id=5470 code = compile( f'# coding={str(encoding)}\n{value}', '', 'eval', ast.PyCF_ONLY_AST | future_flags, ) if isinstance(code, ast.Expression): body = code.body if isinstance(body, ast.Str): return body.s if isinstance(body, ast.JoinedStr): # f-string if all(isinstance(node, ast.Str) for node in body.values): return ''.join(node.s for node in body.values) if all(isinstance(node, ast.Constant) for node in body.values): return ''.join(str(node.value) for node in body.values) # TODO: we could raise an error or warning when not all nodes are constants return None def extract_javascript( fileobj: _FileObj, keywords: Mapping[str, _Keyword], comment_tags: Collection[str], options: _JSOptions, lineno: int = 1, ) -> Generator[_ExtractionResult, None, None]: """Extract messages from JavaScript source code. :param fileobj: the seekable, file-like object the messages should be extracted from :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions :param comment_tags: a list of translator tags to search for and include in the results :param options: a dictionary of additional options (optional) Supported options are: * `jsx` -- set to false to disable JSX/E4X support. * `template_string` -- if `True`, supports gettext(`key`) * `parse_template_string` -- if `True` will parse the contents of javascript template strings. :param lineno: line number offset (for parsing embedded fragments) """ from babel.messages.jslexer import Token, tokenize, unquote_string funcname = message_lineno = None messages = [] last_argument = None translator_comments = [] concatenate_next = False encoding = options.get('encoding', 'utf-8') last_token = None call_stack = -1 dotted = any('.' in kw for kw in keywords) for token in tokenize( fileobj.read().decode(encoding), jsx=options.get("jsx", True), template_string=options.get("template_string", True), dotted=dotted, lineno=lineno, ): if ( # Turn keyword`foo` expressions into keyword("foo") calls: funcname and # have a keyword... (last_token and last_token.type == 'name') and # we've seen nothing after the keyword... token.type == 'template_string' # this is a template string ): message_lineno = token.lineno messages = [unquote_string(token.value)] call_stack = 0 token = Token('operator', ')', token.lineno) if options.get('parse_template_string') and not funcname and token.type == 'template_string': yield from parse_template_string(token.value, keywords, comment_tags, options, token.lineno) elif token.type == 'operator' and token.value == '(': if funcname: message_lineno = token.lineno call_stack += 1 elif call_stack == -1 and token.type == 'linecomment': value = token.value[2:].strip() if translator_comments and \ translator_comments[-1][0] == token.lineno - 1: translator_comments.append((token.lineno, value)) continue for comment_tag in comment_tags: if value.startswith(comment_tag): translator_comments.append((token.lineno, value.strip())) break elif token.type == 'multilinecomment': # only one multi-line comment may precede a translation translator_comments = [] value = token.value[2:-2].strip() for comment_tag in comment_tags: if value.startswith(comment_tag): lines = value.splitlines() if lines: lines[0] = lines[0].strip() lines[1:] = dedent('\n'.join(lines[1:])).splitlines() for offset, line in enumerate(lines): translator_comments.append((token.lineno + offset, line)) break elif funcname and call_stack == 0: if token.type == 'operator' and token.value == ')': if last_argument is not None: messages.append(last_argument) if len(messages) > 1: messages = tuple(messages) elif messages: messages = messages[0] else: messages = None # Comments don't apply unless they immediately precede the # message if translator_comments and \ translator_comments[-1][0] < message_lineno - 1: translator_comments = [] if messages is not None: yield (message_lineno, funcname, messages, [comment[1] for comment in translator_comments]) funcname = message_lineno = last_argument = None concatenate_next = False translator_comments = [] messages = [] call_stack = -1 elif token.type in ('string', 'template_string'): new_value = unquote_string(token.value) if concatenate_next: last_argument = (last_argument or '') + new_value concatenate_next = False else: last_argument = new_value elif token.type == 'operator': if token.value == ',': if last_argument is not None: messages.append(last_argument) last_argument = None else: messages.append(None) concatenate_next = False elif token.value == '+': concatenate_next = True elif call_stack > 0 and token.type == 'operator' \ and token.value == ')': call_stack -= 1 elif funcname and call_stack == -1: funcname = None elif call_stack == -1 and token.type == 'name' and \ token.value in keywords and \ (last_token is None or last_token.type != 'name' or last_token.value != 'function'): funcname = token.value last_token = token def parse_template_string( template_string: str, keywords: Mapping[str, _Keyword], comment_tags: Collection[str], options: _JSOptions, lineno: int = 1, ) -> Generator[_ExtractionResult, None, None]: """Parse JavaScript template string. :param template_string: the template string to be parsed :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions :param comment_tags: a list of translator tags to search for and include in the results :param options: a dictionary of additional options (optional) :param lineno: starting line number (optional) """ from babel.messages.jslexer import line_re prev_character = None level = 0 inside_str = False expression_contents = '' for character in template_string[1:-1]: if not inside_str and character in ('"', "'", '`'): inside_str = character elif inside_str == character and prev_character != r'\\': inside_str = False if level: expression_contents += character if not inside_str: if character == '{' and prev_character == '$': level += 1 elif level and character == '}': level -= 1 if level == 0 and expression_contents: expression_contents = expression_contents[0:-1] fake_file_obj = io.BytesIO(expression_contents.encode()) yield from extract_javascript(fake_file_obj, keywords, comment_tags, options, lineno) lineno += len(line_re.findall(expression_contents)) expression_contents = '' prev_character = character babel-2.14.0/babel/messages/plurals.py0000644000175000017500000001622714536056757017133 0ustar nileshnilesh""" babel.messages.plurals ~~~~~~~~~~~~~~~~~~~~~~ Plural form definitions. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations from operator import itemgetter from babel.core import Locale, default_locale # XXX: remove this file, duplication with babel.plural LC_CTYPE: str | None = default_locale('LC_CTYPE') PLURALS: dict[str, tuple[int, str]] = { # Afar # 'aa': (), # Abkhazian # 'ab': (), # Avestan # 'ae': (), # Afrikaans - From Pootle's PO's 'af': (2, '(n != 1)'), # Akan # 'ak': (), # Amharic # 'am': (), # Aragonese # 'an': (), # Arabic - From Pootle's PO's 'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=0 && n%100<=2 ? 4 : 5)'), # Assamese # 'as': (), # Avaric # 'av': (), # Aymara # 'ay': (), # Azerbaijani # 'az': (), # Bashkir # 'ba': (), # Belarusian 'be': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Bulgarian - From Pootle's PO's 'bg': (2, '(n != 1)'), # Bihari # 'bh': (), # Bislama # 'bi': (), # Bambara # 'bm': (), # Bengali - From Pootle's PO's 'bn': (2, '(n != 1)'), # Tibetan - as discussed in private with Andrew West 'bo': (1, '0'), # Breton 'br': ( 6, '(n==1 ? 0 : n%10==1 && n%100!=11 && n%100!=71 && n%100!=91 ? 1 : n%10==2 && n%100!=12 && n%100!=72 && ' 'n%100!=92 ? 2 : (n%10==3 || n%10==4 || n%10==9) && n%100!=13 && n%100!=14 && n%100!=19 && n%100!=73 && ' 'n%100!=74 && n%100!=79 && n%100!=93 && n%100!=94 && n%100!=99 ? 3 : n%1000000==0 ? 4 : 5)', ), # Bosnian 'bs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Catalan - From Pootle's PO's 'ca': (2, '(n != 1)'), # Chechen # 'ce': (), # Chamorro # 'ch': (), # Corsican # 'co': (), # Cree # 'cr': (), # Czech 'cs': (3, '((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)'), # Church Slavic # 'cu': (), # Chuvash 'cv': (1, '0'), # Welsh 'cy': (5, '(n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 0)'), # Danish 'da': (2, '(n != 1)'), # German 'de': (2, '(n != 1)'), # Divehi # 'dv': (), # Dzongkha 'dz': (1, '0'), # Greek 'el': (2, '(n != 1)'), # English 'en': (2, '(n != 1)'), # Esperanto 'eo': (2, '(n != 1)'), # Spanish 'es': (2, '(n != 1)'), # Estonian 'et': (2, '(n != 1)'), # Basque - From Pootle's PO's 'eu': (2, '(n != 1)'), # Persian - From Pootle's PO's 'fa': (1, '0'), # Finnish 'fi': (2, '(n != 1)'), # French 'fr': (2, '(n > 1)'), # Friulian - From Pootle's PO's 'fur': (2, '(n > 1)'), # Irish 'ga': (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)'), # Galician - From Pootle's PO's 'gl': (2, '(n != 1)'), # Hausa - From Pootle's PO's 'ha': (2, '(n != 1)'), # Hebrew 'he': (2, '(n != 1)'), # Hindi - From Pootle's PO's 'hi': (2, '(n != 1)'), # Croatian 'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Hungarian 'hu': (1, '0'), # Armenian - From Pootle's PO's 'hy': (1, '0'), # Icelandic - From Pootle's PO's 'is': (2, '(n%10==1 && n%100!=11 ? 0 : 1)'), # Italian 'it': (2, '(n != 1)'), # Japanese 'ja': (1, '0'), # Georgian - From Pootle's PO's 'ka': (1, '0'), # Kongo - From Pootle's PO's 'kg': (2, '(n != 1)'), # Khmer - From Pootle's PO's 'km': (1, '0'), # Korean 'ko': (1, '0'), # Kurdish - From Pootle's PO's 'ku': (2, '(n != 1)'), # Lao - Another member of the Tai language family, like Thai. 'lo': (1, '0'), # Lithuanian 'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Latvian 'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'), # Maltese - From Pootle's PO's 'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>=1 && n%100<=10) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'), # Norwegian Bokmål 'nb': (2, '(n != 1)'), # Dutch 'nl': (2, '(n != 1)'), # Norwegian Nynorsk 'nn': (2, '(n != 1)'), # Norwegian 'no': (2, '(n != 1)'), # Punjabi - From Pootle's PO's 'pa': (2, '(n != 1)'), # Polish 'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Portuguese 'pt': (2, '(n != 1)'), # Brazilian 'pt_BR': (2, '(n > 1)'), # Romanian - From Pootle's PO's 'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'), # Russian 'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Slovak 'sk': (3, '((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)'), # Slovenian 'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'), # Serbian - From Pootle's PO's 'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Southern Sotho - From Pootle's PO's 'st': (2, '(n != 1)'), # Swedish 'sv': (2, '(n != 1)'), # Thai 'th': (1, '0'), # Turkish 'tr': (1, '0'), # Ukrainian 'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), # Venda - From Pootle's PO's 've': (2, '(n != 1)'), # Vietnamese - From Pootle's PO's 'vi': (1, '0'), # Xhosa - From Pootle's PO's 'xh': (2, '(n != 1)'), # Chinese - From Pootle's PO's (modified) 'zh': (1, '0'), } DEFAULT_PLURAL: tuple[int, str] = (2, '(n != 1)') class _PluralTuple(tuple): """A tuple with plural information.""" __slots__ = () num_plurals = property(itemgetter(0), doc=""" The number of plurals used by the locale.""") plural_expr = property(itemgetter(1), doc=""" The plural expression used by the locale.""") plural_forms = property(lambda x: 'nplurals={}; plural={};'.format(*x), doc=""" The plural expression used by the catalog or locale.""") def __str__(self) -> str: return self.plural_forms def get_plural(locale: str | None = LC_CTYPE) -> _PluralTuple: """A tuple with the information catalogs need to perform proper pluralization. The first item of the tuple is the number of plural forms, the second the plural expression. >>> get_plural(locale='en') (2, '(n != 1)') >>> get_plural(locale='ga') (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)') The object returned is a special tuple with additional members: >>> tup = get_plural("ja") >>> tup.num_plurals 1 >>> tup.plural_expr '0' >>> tup.plural_forms 'nplurals=1; plural=0;' Converting the tuple into a string prints the plural forms for a gettext catalog: >>> str(tup) 'nplurals=1; plural=0;' """ locale = Locale.parse(locale) try: tup = PLURALS[str(locale)] except KeyError: try: tup = PLURALS[locale.language] except KeyError: tup = DEFAULT_PLURAL return _PluralTuple(tup) babel-2.14.0/babel/messages/__init__.py0000644000175000017500000000053514536056757017203 0ustar nileshnilesh""" babel.messages ~~~~~~~~~~~~~~ Support for ``gettext`` message catalogs. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from babel.messages.catalog import ( Catalog, Message, TranslationError, ) __all__ = [ "Catalog", "Message", "TranslationError", ] babel-2.14.0/babel/messages/catalog.py0000644000175000017500000010671614536056757017066 0ustar nileshnilesh""" babel.messages.catalog ~~~~~~~~~~~~~~~~~~~~~~ Data structures for message catalogs. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import datetime import re from collections import OrderedDict from collections.abc import Iterable, Iterator from copy import copy from difflib import SequenceMatcher from email import message_from_string from heapq import nlargest from typing import TYPE_CHECKING from babel import __version__ as VERSION from babel.core import Locale, UnknownLocaleError from babel.dates import format_datetime from babel.messages.plurals import get_plural from babel.util import LOCALTZ, FixedOffsetTimezone, _cmp, distinct if TYPE_CHECKING: from typing_extensions import TypeAlias _MessageID: TypeAlias = str | tuple[str, ...] | list[str] __all__ = ['Message', 'Catalog', 'TranslationError'] def get_close_matches(word, possibilities, n=3, cutoff=0.6): """A modified version of ``difflib.get_close_matches``. It just passes ``autojunk=False`` to the ``SequenceMatcher``, to work around https://github.com/python/cpython/issues/90825. """ if not n > 0: # pragma: no cover raise ValueError(f"n must be > 0: {n!r}") if not 0.0 <= cutoff <= 1.0: # pragma: no cover raise ValueError(f"cutoff must be in [0.0, 1.0]: {cutoff!r}") result = [] s = SequenceMatcher(autojunk=False) # only line changed from difflib.py s.set_seq2(word) for x in possibilities: s.set_seq1(x) if s.real_quick_ratio() >= cutoff and \ s.quick_ratio() >= cutoff and \ s.ratio() >= cutoff: result.append((s.ratio(), x)) # Move the best scorers to head of list result = nlargest(n, result) # Strip scores for the best n matches return [x for score, x in result] PYTHON_FORMAT = re.compile(r''' \% (?:\(([\w]*)\))? ( [-#0\ +]?(?:\*|[\d]+)? (?:\.(?:\*|[\d]+))? [hlL]? ) ([diouxXeEfFgGcrs%]) ''', re.VERBOSE) def _parse_datetime_header(value: str) -> datetime.datetime: match = re.match(r'^(?P.*?)(?P[+-]\d{4})?$', value) dt = datetime.datetime.strptime(match.group('datetime'), '%Y-%m-%d %H:%M') # Separate the offset into a sign component, hours, and # minutes tzoffset = match.group('tzoffset') if tzoffset is not None: plus_minus_s, rest = tzoffset[0], tzoffset[1:] hours_offset_s, mins_offset_s = rest[:2], rest[2:] # Make them all integers plus_minus = int(f"{plus_minus_s}1") hours_offset = int(hours_offset_s) mins_offset = int(mins_offset_s) # Calculate net offset net_mins_offset = hours_offset * 60 net_mins_offset += mins_offset net_mins_offset *= plus_minus # Create an offset object tzoffset = FixedOffsetTimezone(net_mins_offset) # Store the offset in a datetime object dt = dt.replace(tzinfo=tzoffset) return dt class Message: """Representation of a single message in a catalog.""" def __init__( self, id: _MessageID, string: _MessageID | None = '', locations: Iterable[tuple[str, int]] = (), flags: Iterable[str] = (), auto_comments: Iterable[str] = (), user_comments: Iterable[str] = (), previous_id: _MessageID = (), lineno: int | None = None, context: str | None = None, ) -> None: """Create the message object. :param id: the message ID, or a ``(singular, plural)`` tuple for pluralizable messages :param string: the translated message string, or a ``(singular, plural)`` tuple for pluralizable messages :param locations: a sequence of ``(filename, lineno)`` tuples :param flags: a set or sequence of flags :param auto_comments: a sequence of automatic comments for the message :param user_comments: a sequence of user comments for the message :param previous_id: the previous message ID, or a ``(singular, plural)`` tuple for pluralizable messages :param lineno: the line number on which the msgid line was found in the PO file, if any :param context: the message context """ self.id = id if not string and self.pluralizable: string = ('', '') self.string = string self.locations = list(distinct(locations)) self.flags = set(flags) if id and self.python_format: self.flags.add('python-format') else: self.flags.discard('python-format') self.auto_comments = list(distinct(auto_comments)) self.user_comments = list(distinct(user_comments)) if isinstance(previous_id, str): self.previous_id = [previous_id] else: self.previous_id = list(previous_id) self.lineno = lineno self.context = context def __repr__(self) -> str: return f"<{type(self).__name__} {self.id!r} (flags: {list(self.flags)!r})>" def __cmp__(self, other: object) -> int: """Compare Messages, taking into account plural ids""" def values_to_compare(obj): if isinstance(obj, Message) and obj.pluralizable: return obj.id[0], obj.context or '' return obj.id, obj.context or '' return _cmp(values_to_compare(self), values_to_compare(other)) def __gt__(self, other: object) -> bool: return self.__cmp__(other) > 0 def __lt__(self, other: object) -> bool: return self.__cmp__(other) < 0 def __ge__(self, other: object) -> bool: return self.__cmp__(other) >= 0 def __le__(self, other: object) -> bool: return self.__cmp__(other) <= 0 def __eq__(self, other: object) -> bool: return self.__cmp__(other) == 0 def __ne__(self, other: object) -> bool: return self.__cmp__(other) != 0 def is_identical(self, other: Message) -> bool: """Checks whether messages are identical, taking into account all properties. """ assert isinstance(other, Message) return self.__dict__ == other.__dict__ def clone(self) -> Message: return Message(*map(copy, (self.id, self.string, self.locations, self.flags, self.auto_comments, self.user_comments, self.previous_id, self.lineno, self.context))) def check(self, catalog: Catalog | None = None) -> list[TranslationError]: """Run various validation checks on the message. Some validations are only performed if the catalog is provided. This method returns a sequence of `TranslationError` objects. :rtype: ``iterator`` :param catalog: A catalog instance that is passed to the checkers :see: `Catalog.check` for a way to perform checks for all messages in a catalog. """ from babel.messages.checkers import checkers errors: list[TranslationError] = [] for checker in checkers: try: checker(catalog, self) except TranslationError as e: errors.append(e) return errors @property def fuzzy(self) -> bool: """Whether the translation is fuzzy. >>> Message('foo').fuzzy False >>> msg = Message('foo', 'foo', flags=['fuzzy']) >>> msg.fuzzy True >>> msg :type: `bool`""" return 'fuzzy' in self.flags @property def pluralizable(self) -> bool: """Whether the message is plurizable. >>> Message('foo').pluralizable False >>> Message(('foo', 'bar')).pluralizable True :type: `bool`""" return isinstance(self.id, (list, tuple)) @property def python_format(self) -> bool: """Whether the message contains Python-style parameters. >>> Message('foo %(name)s bar').python_format True >>> Message(('foo %(name)s', 'foo %(name)s')).python_format True :type: `bool`""" ids = self.id if not isinstance(ids, (list, tuple)): ids = [ids] return any(PYTHON_FORMAT.search(id) for id in ids) class TranslationError(Exception): """Exception thrown by translation checkers when invalid message translations are encountered.""" DEFAULT_HEADER = """\ # Translations template for PROJECT. # Copyright (C) YEAR ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , YEAR. #""" def parse_separated_header(value: str) -> dict[str, str]: # Adapted from https://peps.python.org/pep-0594/#cgi from email.message import Message m = Message() m['content-type'] = value return dict(m.get_params()) class Catalog: """Representation of a message catalog.""" def __init__( self, locale: str | Locale | None = None, domain: str | None = None, header_comment: str | None = DEFAULT_HEADER, project: str | None = None, version: str | None = None, copyright_holder: str | None = None, msgid_bugs_address: str | None = None, creation_date: datetime.datetime | str | None = None, revision_date: datetime.datetime | datetime.time | float | str | None = None, last_translator: str | None = None, language_team: str | None = None, charset: str | None = None, fuzzy: bool = True, ) -> None: """Initialize the catalog object. :param locale: the locale identifier or `Locale` object, or `None` if the catalog is not bound to a locale (which basically means it's a template) :param domain: the message domain :param header_comment: the header comment as string, or `None` for the default header :param project: the project's name :param version: the project's version :param copyright_holder: the copyright holder of the catalog :param msgid_bugs_address: the email address or URL to submit bug reports to :param creation_date: the date the catalog was created :param revision_date: the date the catalog was revised :param last_translator: the name and email of the last translator :param language_team: the name and email of the language team :param charset: the encoding to use in the output (defaults to utf-8) :param fuzzy: the fuzzy bit on the catalog header """ self.domain = domain self.locale = locale self._header_comment = header_comment self._messages: OrderedDict[str | tuple[str, str], Message] = OrderedDict() self.project = project or 'PROJECT' self.version = version or 'VERSION' self.copyright_holder = copyright_holder or 'ORGANIZATION' self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' self.last_translator = last_translator or 'FULL NAME ' """Name and email address of the last translator.""" self.language_team = language_team or 'LANGUAGE ' """Name and email address of the language team.""" self.charset = charset or 'utf-8' if creation_date is None: creation_date = datetime.datetime.now(LOCALTZ) elif isinstance(creation_date, datetime.datetime) and not creation_date.tzinfo: creation_date = creation_date.replace(tzinfo=LOCALTZ) self.creation_date = creation_date if revision_date is None: revision_date = 'YEAR-MO-DA HO:MI+ZONE' elif isinstance(revision_date, datetime.datetime) and not revision_date.tzinfo: revision_date = revision_date.replace(tzinfo=LOCALTZ) self.revision_date = revision_date self.fuzzy = fuzzy # Dictionary of obsolete messages self.obsolete: OrderedDict[str | tuple[str, str], Message] = OrderedDict() self._num_plurals = None self._plural_expr = None def _set_locale(self, locale: Locale | str | None) -> None: if locale is None: self._locale_identifier = None self._locale = None return if isinstance(locale, Locale): self._locale_identifier = str(locale) self._locale = locale return if isinstance(locale, str): self._locale_identifier = str(locale) try: self._locale = Locale.parse(locale) except UnknownLocaleError: self._locale = None return raise TypeError(f"`locale` must be a Locale, a locale identifier string, or None; got {locale!r}") def _get_locale(self) -> Locale | None: return self._locale def _get_locale_identifier(self) -> str | None: return self._locale_identifier locale = property(_get_locale, _set_locale) locale_identifier = property(_get_locale_identifier) def _get_header_comment(self) -> str: comment = self._header_comment year = datetime.datetime.now(LOCALTZ).strftime('%Y') if hasattr(self.revision_date, 'strftime'): year = self.revision_date.strftime('%Y') comment = comment.replace('PROJECT', self.project) \ .replace('VERSION', self.version) \ .replace('YEAR', year) \ .replace('ORGANIZATION', self.copyright_holder) locale_name = (self.locale.english_name if self.locale else self.locale_identifier) if locale_name: comment = comment.replace("Translations template", f"{locale_name} translations") return comment def _set_header_comment(self, string: str | None) -> None: self._header_comment = string header_comment = property(_get_header_comment, _set_header_comment, doc="""\ The header comment for the catalog. >>> catalog = Catalog(project='Foobar', version='1.0', ... copyright_holder='Foo Company') >>> print(catalog.header_comment) #doctest: +ELLIPSIS # Translations template for Foobar. # Copyright (C) ... Foo Company # This file is distributed under the same license as the Foobar project. # FIRST AUTHOR , .... # The header can also be set from a string. Any known upper-case variables will be replaced when the header is retrieved again: >>> catalog = Catalog(project='Foobar', version='1.0', ... copyright_holder='Foo Company') >>> catalog.header_comment = '''\\ ... # The POT for my really cool PROJECT project. ... # Copyright (C) 1990-2003 ORGANIZATION ... # This file is distributed under the same license as the PROJECT ... # project. ... #''' >>> print(catalog.header_comment) # The POT for my really cool Foobar project. # Copyright (C) 1990-2003 Foo Company # This file is distributed under the same license as the Foobar # project. # :type: `unicode` """) def _get_mime_headers(self) -> list[tuple[str, str]]: headers: list[tuple[str, str]] = [] headers.append(("Project-Id-Version", f"{self.project} {self.version}")) headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) headers.append(('POT-Creation-Date', format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', locale='en'))) if isinstance(self.revision_date, (datetime.datetime, datetime.time, int, float)): headers.append(('PO-Revision-Date', format_datetime(self.revision_date, 'yyyy-MM-dd HH:mmZ', locale='en'))) else: headers.append(('PO-Revision-Date', self.revision_date)) headers.append(('Last-Translator', self.last_translator)) if self.locale_identifier: headers.append(('Language', str(self.locale_identifier))) if self.locale_identifier and ('LANGUAGE' in self.language_team): headers.append(('Language-Team', self.language_team.replace('LANGUAGE', str(self.locale_identifier)))) else: headers.append(('Language-Team', self.language_team)) if self.locale is not None: headers.append(('Plural-Forms', self.plural_forms)) headers.append(('MIME-Version', '1.0')) headers.append(("Content-Type", f"text/plain; charset={self.charset}")) headers.append(('Content-Transfer-Encoding', '8bit')) headers.append(("Generated-By", f"Babel {VERSION}\n")) return headers def _force_text(self, s: str | bytes, encoding: str = 'utf-8', errors: str = 'strict') -> str: if isinstance(s, str): return s if isinstance(s, bytes): return s.decode(encoding, errors) return str(s) def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None: for name, value in headers: name = self._force_text(name.lower(), encoding=self.charset) value = self._force_text(value, encoding=self.charset) if name == 'project-id-version': parts = value.split(' ') self.project = ' '.join(parts[:-1]) self.version = parts[-1] elif name == 'report-msgid-bugs-to': self.msgid_bugs_address = value elif name == 'last-translator': self.last_translator = value elif name == 'language': value = value.replace('-', '_') self._set_locale(value) elif name == 'language-team': self.language_team = value elif name == 'content-type': params = parse_separated_header(value) if 'charset' in params: self.charset = params['charset'].lower() elif name == 'plural-forms': params = parse_separated_header(f" ;{value}") self._num_plurals = int(params.get('nplurals', 2)) self._plural_expr = params.get('plural', '(n != 1)') elif name == 'pot-creation-date': self.creation_date = _parse_datetime_header(value) elif name == 'po-revision-date': # Keep the value if it's not the default one if 'YEAR' not in value: self.revision_date = _parse_datetime_header(value) mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ The MIME headers of the catalog, used for the special ``msgid ""`` entry. The behavior of this property changes slightly depending on whether a locale is set or not, the latter indicating that the catalog is actually a template for actual translations. Here's an example of the output for such a catalog template: >>> from babel.dates import UTC >>> from datetime import datetime >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) >>> catalog = Catalog(project='Foobar', version='1.0', ... creation_date=created) >>> for name, value in catalog.mime_headers: ... print('%s: %s' % (name, value)) Project-Id-Version: Foobar 1.0 Report-Msgid-Bugs-To: EMAIL@ADDRESS POT-Creation-Date: 1990-04-01 15:30+0000 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel ... And here's an example of the output when the locale is set: >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', ... creation_date=created, revision_date=revised, ... last_translator='John Doe ', ... language_team='de_DE ') >>> for name, value in catalog.mime_headers: ... print('%s: %s' % (name, value)) Project-Id-Version: Foobar 1.0 Report-Msgid-Bugs-To: EMAIL@ADDRESS POT-Creation-Date: 1990-04-01 15:30+0000 PO-Revision-Date: 1990-08-03 12:00+0000 Last-Translator: John Doe Language: de_DE Language-Team: de_DE Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel ... :type: `list` """) @property def num_plurals(self) -> int: """The number of plurals used by the catalog or locale. >>> Catalog(locale='en').num_plurals 2 >>> Catalog(locale='ga').num_plurals 5 :type: `int`""" if self._num_plurals is None: num = 2 if self.locale: num = get_plural(self.locale)[0] self._num_plurals = num return self._num_plurals @property def plural_expr(self) -> str: """The plural expression used by the catalog or locale. >>> Catalog(locale='en').plural_expr '(n != 1)' >>> Catalog(locale='ga').plural_expr '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)' >>> Catalog(locale='ding').plural_expr # unknown locale '(n != 1)' :type: `str`""" if self._plural_expr is None: expr = '(n != 1)' if self.locale: expr = get_plural(self.locale)[1] self._plural_expr = expr return self._plural_expr @property def plural_forms(self) -> str: """Return the plural forms declaration for the locale. >>> Catalog(locale='en').plural_forms 'nplurals=2; plural=(n != 1);' >>> Catalog(locale='pt_BR').plural_forms 'nplurals=2; plural=(n > 1);' :type: `str`""" return f"nplurals={self.num_plurals}; plural={self.plural_expr};" def __contains__(self, id: _MessageID) -> bool: """Return whether the catalog has a message with the specified ID.""" return self._key_for(id) in self._messages def __len__(self) -> int: """The number of messages in the catalog. This does not include the special ``msgid ""`` entry.""" return len(self._messages) def __iter__(self) -> Iterator[Message]: """Iterates through all the entries in the catalog, in the order they were added, yielding a `Message` object for every entry. :rtype: ``iterator``""" buf = [] for name, value in self.mime_headers: buf.append(f"{name}: {value}") flags = set() if self.fuzzy: flags |= {'fuzzy'} yield Message('', '\n'.join(buf), flags=flags) for key in self._messages: yield self._messages[key] def __repr__(self) -> str: locale = '' if self.locale: locale = f" {self.locale}" return f"<{type(self).__name__} {self.domain!r}{locale}>" def __delitem__(self, id: _MessageID) -> None: """Delete the message with the specified ID.""" self.delete(id) def __getitem__(self, id: _MessageID) -> Message: """Return the message with the specified ID. :param id: the message ID """ return self.get(id) def __setitem__(self, id: _MessageID, message: Message) -> None: """Add or update the message with the specified ID. >>> catalog = Catalog() >>> catalog[u'foo'] = Message(u'foo') >>> catalog[u'foo'] If a message with that ID is already in the catalog, it is updated to include the locations and flags of the new message. >>> catalog = Catalog() >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) >>> catalog[u'foo'].locations [('main.py', 1)] >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) >>> catalog[u'foo'].locations [('main.py', 1), ('utils.py', 5)] :param id: the message ID :param message: the `Message` object """ assert isinstance(message, Message), 'expected a Message object' key = self._key_for(id, message.context) current = self._messages.get(key) if current: if message.pluralizable and not current.pluralizable: # The new message adds pluralization current.id = message.id current.string = message.string current.locations = list(distinct(current.locations + message.locations)) current.auto_comments = list(distinct(current.auto_comments + message.auto_comments)) current.user_comments = list(distinct(current.user_comments + message.user_comments)) current.flags |= message.flags message = current elif id == '': # special treatment for the header message self.mime_headers = message_from_string(message.string).items() self.header_comment = "\n".join([f"# {c}".rstrip() for c in message.user_comments]) self.fuzzy = message.fuzzy else: if isinstance(id, (list, tuple)): assert isinstance(message.string, (list, tuple)), \ f"Expected sequence but got {type(message.string)}" self._messages[key] = message def add( self, id: _MessageID, string: _MessageID | None = None, locations: Iterable[tuple[str, int]] = (), flags: Iterable[str] = (), auto_comments: Iterable[str] = (), user_comments: Iterable[str] = (), previous_id: _MessageID = (), lineno: int | None = None, context: str | None = None, ) -> Message: """Add or update the message with the specified ID. >>> catalog = Catalog() >>> catalog.add(u'foo') >>> catalog[u'foo'] This method simply constructs a `Message` object with the given arguments and invokes `__setitem__` with that object. :param id: the message ID, or a ``(singular, plural)`` tuple for pluralizable messages :param string: the translated message string, or a ``(singular, plural)`` tuple for pluralizable messages :param locations: a sequence of ``(filename, lineno)`` tuples :param flags: a set or sequence of flags :param auto_comments: a sequence of automatic comments :param user_comments: a sequence of user comments :param previous_id: the previous message ID, or a ``(singular, plural)`` tuple for pluralizable messages :param lineno: the line number on which the msgid line was found in the PO file, if any :param context: the message context """ message = Message(id, string, list(locations), flags, auto_comments, user_comments, previous_id, lineno=lineno, context=context) self[id] = message return message def check(self) -> Iterable[tuple[Message, list[TranslationError]]]: """Run various validation checks on the translations in the catalog. For every message which fails validation, this method yield a ``(message, errors)`` tuple, where ``message`` is the `Message` object and ``errors`` is a sequence of `TranslationError` objects. :rtype: ``generator`` of ``(message, errors)`` """ for message in self._messages.values(): errors = message.check(catalog=self) if errors: yield message, errors def get(self, id: _MessageID, context: str | None = None) -> Message | None: """Return the message with the specified ID and context. :param id: the message ID :param context: the message context, or ``None`` for no context """ return self._messages.get(self._key_for(id, context)) def delete(self, id: _MessageID, context: str | None = None) -> None: """Delete the message with the specified ID and context. :param id: the message ID :param context: the message context, or ``None`` for no context """ key = self._key_for(id, context) if key in self._messages: del self._messages[key] def update( self, template: Catalog, no_fuzzy_matching: bool = False, update_header_comment: bool = False, keep_user_comments: bool = True, update_creation_date: bool = True, ) -> None: """Update the catalog based on the given template catalog. >>> from babel.messages import Catalog >>> template = Catalog() >>> template.add('green', locations=[('main.py', 99)]) >>> template.add('blue', locations=[('main.py', 100)]) >>> template.add(('salad', 'salads'), locations=[('util.py', 42)]) >>> catalog = Catalog(locale='de_DE') >>> catalog.add('blue', u'blau', locations=[('main.py', 98)]) >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)]) >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), ... locations=[('util.py', 38)]) >>> catalog.update(template) >>> len(catalog) 3 >>> msg1 = catalog['green'] >>> msg1.string >>> msg1.locations [('main.py', 99)] >>> msg2 = catalog['blue'] >>> msg2.string u'blau' >>> msg2.locations [('main.py', 100)] >>> msg3 = catalog['salad'] >>> msg3.string (u'Salat', u'Salate') >>> msg3.locations [('util.py', 42)] Messages that are in the catalog but not in the template are removed from the main collection, but can still be accessed via the `obsolete` member: >>> 'head' in catalog False >>> list(catalog.obsolete.values()) [] :param template: the reference catalog, usually read from a POT file :param no_fuzzy_matching: whether to use fuzzy matching of message IDs """ messages = self._messages remaining = messages.copy() self._messages = OrderedDict() # Prepare for fuzzy matching fuzzy_candidates = {} if not no_fuzzy_matching: for msgid in messages: if msgid and messages[msgid].string: key = self._key_for(msgid) ctxt = messages[msgid].context fuzzy_candidates[self._to_fuzzy_match_key(key)] = (key, ctxt) fuzzy_matches = set() def _merge(message: Message, oldkey: tuple[str, str] | str, newkey: tuple[str, str] | str) -> None: message = message.clone() fuzzy = False if oldkey != newkey: fuzzy = True fuzzy_matches.add(oldkey) oldmsg = messages.get(oldkey) assert oldmsg is not None if isinstance(oldmsg.id, str): message.previous_id = [oldmsg.id] else: message.previous_id = list(oldmsg.id) else: oldmsg = remaining.pop(oldkey, None) assert oldmsg is not None message.string = oldmsg.string if keep_user_comments: message.user_comments = list(distinct(oldmsg.user_comments)) if isinstance(message.id, (list, tuple)): if not isinstance(message.string, (list, tuple)): fuzzy = True message.string = tuple( [message.string] + ([''] * (len(message.id) - 1)), ) elif len(message.string) != self.num_plurals: fuzzy = True message.string = tuple(message.string[:len(oldmsg.string)]) elif isinstance(message.string, (list, tuple)): fuzzy = True message.string = message.string[0] message.flags |= oldmsg.flags if fuzzy: message.flags |= {'fuzzy'} self[message.id] = message for message in template: if message.id: key = self._key_for(message.id, message.context) if key in messages: _merge(message, key, key) else: if not no_fuzzy_matching: # do some fuzzy matching with difflib matches = get_close_matches( self._to_fuzzy_match_key(key), fuzzy_candidates.keys(), 1, ) if matches: modified_key = matches[0] newkey, newctxt = fuzzy_candidates[modified_key] if newctxt is not None: newkey = newkey, newctxt _merge(message, newkey, key) continue self[message.id] = message for msgid in remaining: if no_fuzzy_matching or msgid not in fuzzy_matches: self.obsolete[msgid] = remaining[msgid] if update_header_comment: # Allow the updated catalog's header to be rewritten based on the # template's header self.header_comment = template.header_comment # Make updated catalog's POT-Creation-Date equal to the template # used to update the catalog if update_creation_date: self.creation_date = template.creation_date def _to_fuzzy_match_key(self, key: tuple[str, str] | str) -> str: """Converts a message key to a string suitable for fuzzy matching.""" if isinstance(key, tuple): matchkey = key[0] # just the msgid, no context else: matchkey = key return matchkey.lower().strip() def _key_for(self, id: _MessageID, context: str | None = None) -> tuple[str, str] | str: """The key for a message is just the singular ID even for pluralizable messages, but is a ``(msgid, msgctxt)`` tuple for context-specific messages. """ key = id if isinstance(key, (list, tuple)): key = id[0] if context is not None: key = (key, context) return key def is_identical(self, other: Catalog) -> bool: """Checks if catalogs are identical, taking into account messages and headers. """ assert isinstance(other, Catalog) for key in self._messages.keys() | other._messages.keys(): message_1 = self.get(key) message_2 = other.get(key) if ( message_1 is None or message_2 is None or not message_1.is_identical(message_2) ): return False return dict(self.mime_headers) == dict(other.mime_headers) babel-2.14.0/babel/messages/jslexer.py0000644000175000017500000001576114536056757017127 0ustar nileshnilesh""" babel.messages.jslexer ~~~~~~~~~~~~~~~~~~~~~~ A simple JavaScript 1.5 lexer which is used for the JavaScript extractor. :copyright: (c) 2013-2023 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import re from collections.abc import Generator from typing import NamedTuple operators: list[str] = sorted([ '+', '-', '*', '%', '!=', '==', '<', '>', '<=', '>=', '=', '+=', '-=', '*=', '%=', '<<', '>>', '>>>', '<<=', '>>=', '>>>=', '&', '&=', '|', '|=', '&&', '||', '^', '^=', '(', ')', '[', ']', '{', '}', '!', '--', '++', '~', ',', ';', '.', ':', ], key=len, reverse=True) escapes: dict[str, str] = {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'} name_re = re.compile(r'[\w$_][\w\d$_]*', re.UNICODE) dotted_name_re = re.compile(r'[\w$_][\w\d$_.]*[\w\d$_.]', re.UNICODE) division_re = re.compile(r'/=?') regex_re = re.compile(r'/(?:[^/\\]*(?:\\.[^/\\]*)*)/[a-zA-Z]*', re.DOTALL) line_re = re.compile(r'(\r\n|\n|\r)') line_join_re = re.compile(r'\\' + line_re.pattern) uni_escape_re = re.compile(r'[a-fA-F0-9]{1,4}') hex_escape_re = re.compile(r'[a-fA-F0-9]{1,2}') class Token(NamedTuple): type: str value: str lineno: int _rules: list[tuple[str | None, re.Pattern[str]]] = [ (None, re.compile(r'\s+', re.UNICODE)), (None, re.compile(r'