├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README ├── docs ├── Makefile ├── _static │ └── flask-babel.png ├── conf.py ├── index.rst └── make.bat ├── flask_babelex ├── __init__.py └── _compat.py ├── setup.cfg ├── setup.py ├── tests ├── babel.cfg ├── tests.py └── translations │ ├── de │ └── LC_MESSAGES │ │ ├── messages.mo │ │ ├── messages.po │ │ ├── test.mo │ │ └── test.po │ ├── messages.pot │ └── test.pot └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | *.egg-info 5 | dist/ 6 | docs/_build 7 | .tox/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/mitsuhiko/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | - "pypy" 7 | - "3.3" 8 | 9 | install: 10 | - pip install --upgrade pip 11 | - pip install pytest 12 | - pip install --editable . 13 | 14 | script: make test 15 | 16 | notifications: 17 | email: false 18 | irc: 19 | channels: 20 | - "chat.freenode.net#pocoo" 21 | on_success: change 22 | on_failure: always 23 | use_notice: true 24 | skip_join: true 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 by Serge S. Koval, Armin Ronacher and contributors. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Makefile LICENSE 2 | recursive-include tests * 3 | recursive-exclude tests *.pyc 4 | recursive-exclude tests *.pyo 5 | recursive-include docs * 6 | recursive-exclude docs *.pyc 7 | recursive-exclude docs *.pyo 8 | prune docs/_build 9 | prune docs/_themes/.git 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc test upload-docs 2 | 3 | all: clean-pyc test 4 | 5 | test: 6 | @cd tests; python tests.py 7 | 8 | tox-test: 9 | @tox 10 | 11 | clean-pyc: 12 | find . -name '*.pyc' -exec rm -f {} + 13 | find . -name '*.pyo' -exec rm -f {} + 14 | find . -name '*~' -exec rm -f {} + 15 | 16 | clean: clean-pyc 17 | 18 | upload-docs: 19 | $(MAKE) -C docs html 20 | python setup.py upload_docs 21 | 22 | .PHONY: upload-docs clean-pyc clean tox-test test all 23 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Flask-BabelEx 2 | 3 | *DEPRECATION WARNING* 4 | All Flask-BabelEx features were merged into Flask-Babel and Flask-BabelEx package should no longer be used in your projects. Please migrate. 5 | 6 | Implements i18n and l10n support for Flask. This is fork of the official Flask-Babel 7 | extension with additional features: 8 | 9 | 1. It is possible to use multiple language catalogs in one Flask application; 10 | 2. Localization domains: your extension can package localization file(s) and use them 11 | if necessary; 12 | 3. Does not reload localizations for each request. 13 | 14 | Flask-BabelEx is API-compatible with Flask-Babel. 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FlaskBabel.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FlaskBabel.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/FlaskBabel" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FlaskBabel" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: latex 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_static/flask-babel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjoes/flask-babelex/e395444ff7207e2585217a34ffdf0c5627f7b89b/docs/_static/flask-babel.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask Babel documentation build configuration file, created by 4 | # sphinx-quickstart on Sat May 29 13:52:12 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 20 | sys.path.append(os.path.abspath('_themes')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Flask Babel' 45 | copyright = u'2010, Armin Ronacher' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '1.0' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '1.0' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. Major themes that come with 94 | # Sphinx are currently 'default' and 'sphinxdoc'. 95 | html_theme = 'flask_small' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | html_theme_options = { 101 | 'github_fork': 'mrjoes/flask-babelex' 102 | } 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | html_theme_path = ['_themes'] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = '' 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'FlaskBabeldoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | # The paper size ('letter' or 'a4'). 176 | #latex_paper_size = 'letter' 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #latex_font_size = '10pt' 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ('index', 'FlaskBabel.tex', u'Flask Babel Documentation', 185 | u'Armin Ronacher', 'manual'), 186 | ] 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | #latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | #latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | #latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | #latex_show_urls = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'flaskbabel', u'Flask Babel Documentation', 218 | [u'Armin Ronacher'], 1) 219 | ] 220 | 221 | intersphinx_mapping = {'http://docs.python.org/': None, 222 | 'http://flask.pocoo.org/docs/': None} 223 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-BabelEx 2 | ============= 3 | 4 | .. module:: flask_babelex 5 | 6 | Flask-BabelEx is an extension to `Flask`_ that adds i18n and l10n support to 7 | any Flask application with the help of `babel`_, `pytz`_ and 8 | `speaklater`_. It has builtin support for date formatting with timezone 9 | support as well as a very simple and friendly interface to :mod:`gettext` 10 | translations. 11 | 12 | Installation 13 | ------------ 14 | 15 | Install the extension with one of the following commands:: 16 | 17 | $ easy_install Flask-BabelEx 18 | 19 | or alternatively if you have pip installed:: 20 | 21 | $ pip install Flask-BabelEx 22 | 23 | Please note that Flask-BabelEx requires Jinja 2.5. If you are using an 24 | older version you will have to upgrade or disable the Jinja support. 25 | 26 | 27 | Configuration 28 | ------------- 29 | 30 | To get started all you need to do is to instanciate a :class:`Babel` 31 | object after configuring the application:: 32 | 33 | from flask import Flask 34 | from flask_babelex import Babel 35 | 36 | app = Flask(__name__) 37 | app.config.from_pyfile('mysettings.cfg') 38 | babel = Babel(app) 39 | 40 | The babel object itself can be used to configure the babel support 41 | further. Babel has two configuration values that can be used to change 42 | some internal defaults: 43 | 44 | =========================== ============================================= 45 | `BABEL_DEFAULT_LOCALE` The default locale to use if no locale 46 | selector is registered. This defaults 47 | to ``'en'``. 48 | `BABEL_DEFAULT_TIMEZONE` The timezone to use for user facing dates. 49 | This defaults to ``'UTC'`` which also is the 50 | timezone your application must use internally. 51 | =========================== ============================================= 52 | 53 | For more complex applications you might want to have multiple applications 54 | for different users which is where selector functions come in handy. The 55 | first time the babel extension needs the locale (language code) of the 56 | current user it will call a :meth:`~Babel.localeselector` function, and 57 | the first time the timezone is needed it will call a 58 | :meth:`~Babel.timezoneselector` function. 59 | 60 | If any of these methods return `None` the extension will automatically 61 | fall back to what's in the config. Furthermore for efficiency that 62 | function is called only once and the return value then cached. If you 63 | need to switch the language between a request, you can :func:`refresh` the 64 | cache. 65 | 66 | Example selector functions:: 67 | 68 | from flask import g, request 69 | 70 | @babel.localeselector 71 | def get_locale(): 72 | # if a user is logged in, use the locale from the user settings 73 | user = getattr(g, 'user', None) 74 | if user is not None: 75 | return user.locale 76 | # otherwise try to guess the language from the user accept 77 | # header the browser transmits. We support de/fr/en in this 78 | # example. The best match wins. 79 | return request.accept_languages.best_match(['de', 'fr', 'en']) 80 | 81 | @babel.timezoneselector 82 | def get_timezone(): 83 | user = getattr(g, 'user', None) 84 | if user is not None: 85 | return user.timezone 86 | 87 | The example above assumes that the current user is stored on the 88 | :data:`flask.g` object. 89 | 90 | Formatting Dates 91 | ---------------- 92 | 93 | To format dates you can use the :func:`format_datetime`, 94 | :func:`format_date`, :func:`format_time` and :func:`format_timedelta` 95 | functions. They all accept a :class:`datetime.datetime` (or 96 | :class:`datetime.date`, :class:`datetime.time` and 97 | :class:`datetime.timedelta`) object as first parameter and then optionally 98 | a format string. The application should use naive datetime objects 99 | internally that use UTC as timezone. On formatting it will automatically 100 | convert into the user's timezone in case it differs from UTC. 101 | 102 | To play with the date formatting from the console, you can use the 103 | :meth:`~flask.Flask.test_request_context` method: 104 | 105 | >>> app.test_request_context().push() 106 | 107 | Here some examples: 108 | 109 | >>> from flask_babelex import format_datetime 110 | >>> from datetime import datetime 111 | >>> format_datetime(datetime(1987, 3, 5, 17, 12)) 112 | u'Mar 5, 1987 5:12:00 PM' 113 | >>> format_datetime(datetime(1987, 3, 5, 17, 12), 'full') 114 | u'Thursday, March 5, 1987 5:12:00 PM World (GMT) Time' 115 | >>> format_datetime(datetime(1987, 3, 5, 17, 12), 'short') 116 | u'3/5/87 5:12 PM' 117 | >>> format_datetime(datetime(1987, 3, 5, 17, 12), 'dd mm yyy') 118 | u'05 12 1987' 119 | >>> format_datetime(datetime(1987, 3, 5, 17, 12), 'dd mm yyyy') 120 | u'05 12 1987' 121 | 122 | And again with a different language: 123 | 124 | >>> app.config['BABEL_DEFAULT_LOCALE'] = 'de' 125 | >>> from flask_babelex import refresh; refresh() 126 | >>> format_datetime(datetime(1987, 3, 5, 17, 12), 'EEEE, d. MMMM yyyy H:mm') 127 | u'Donnerstag, 5. M\xe4rz 1987 17:12' 128 | 129 | For more format examples head over to the `babel`_ documentation. 130 | 131 | Using Translations 132 | ------------------ 133 | 134 | The other big part next to date formatting are translations. For that, 135 | Flask uses :mod:`gettext` together with Babel. The idea of gettext is 136 | that you can mark certain strings as translatable and a tool will pick all 137 | those app, collect them in a separate file for you to translate. At 138 | runtime the original strings (which should be English) will be replaced by 139 | the language you selected. 140 | 141 | There are two functions responsible for translating: :func:`gettext` and 142 | :func:`ngettext`. The first to translate singular strings and the second 143 | to translate strings that might become plural. Here some examples:: 144 | 145 | from flask_babelex import gettext, ngettext 146 | 147 | gettext(u'A simple string') 148 | gettext(u'Value: %(value)s', value=42) 149 | ngettext(u'%(num)s Apple', u'%(num)s Apples', number_of_apples) 150 | 151 | Additionally if you want to use constant strings somewhere in your 152 | application and define them outside of a request, you can use a lazy 153 | strings. Lazy strings will not be evaluated until they are actually used. 154 | To use such a lazy string, use the :func:`lazy_gettext` function:: 155 | 156 | from flask_babelex import lazy_gettext 157 | 158 | class MyForm(formlibrary.FormBase): 159 | success_message = lazy_gettext(u'The form was successfully saved.') 160 | 161 | So how does Flask-BabelEx find the translations? Well first you have to 162 | create some. Here is how you do it: 163 | 164 | Translating Applications 165 | ------------------------ 166 | 167 | First you need to mark all the strings you want to translate in your 168 | application with :func:`gettext` or :func:`ngettext`. After that, it's 169 | time to create a ``.pot`` file. A ``.pot`` file contains all the strings 170 | and is the template for a ``.po`` file which contains the translated 171 | strings. Babel can do all that for you. 172 | 173 | First of all you have to get into the folder where you have your 174 | application and create a mapping file. For typical Flask applications, this 175 | is what you want in there: 176 | 177 | .. sourcecode:: ini 178 | 179 | [python: **.py] 180 | [jinja2: **/templates/**.html] 181 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 182 | 183 | Save it as ``babel.cfg`` or something similar next to your application. 184 | Then it's time to run the `pybabel` command that comes with Babel to 185 | extract your strings:: 186 | 187 | $ pybabel extract -F babel.cfg -o messages.pot . 188 | 189 | If you are using the :func:`lazy_gettext` function you should tell pybabel 190 | that it should also look for such function calls:: 191 | 192 | $ pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . 193 | 194 | This will use the mapping from the ``babel.cfg`` file and store the 195 | generated template in ``messages.pot``. Now we can create the first 196 | translation. For example to translate to German use this command:: 197 | 198 | $ pybabel init -i messages.pot -d translations -l de 199 | 200 | ``-d translations`` tells pybabel to store the translations in this 201 | folder. This is where Flask-BabelEx will look for translations. Put it 202 | next to your template folder. 203 | 204 | Now edit the ``translations/de/LC_MESSAGES/messages.po`` file as needed. 205 | Check out some gettext tutorials if you feel lost. 206 | 207 | To compile the translations for use, ``pybabel`` helps again:: 208 | 209 | $ pybabel compile -d translations 210 | 211 | What if the strings change? Create a new ``messages.pot`` like above and 212 | then let ``pybabel`` merge the changes:: 213 | 214 | $ pybabel update -i messages.pot -d translations 215 | 216 | Afterwards some strings might be marked as fuzzy (where it tried to figure 217 | out if a translation matched a changed key). If you have fuzzy entries, 218 | make sure to check them by hand and remove the fuzzy flag before 219 | compiling. 220 | 221 | Flask-BabelEx looks for message catalogs in ``translations`` directory 222 | which should be located under Flask application directory. Default 223 | domain is "messages". 224 | 225 | For example, if you want to have translations for German, Spanish and French, 226 | directory structure should look like this: 227 | 228 | translations/de/LC_MESSAGES/messages.mo 229 | translations/sp/LC_MESSAGES/messages.mo 230 | translations/fr/LC_MESSAGES/messages.mo 231 | 232 | Translation Domains 233 | ------------------- 234 | 235 | By default, Flask-BabelEx will use "messages" domain, which will make it use translations 236 | from the ``messages.mo`` file. It is not very convenient for third-party Flask extensions, 237 | which might want to localize themselves without requiring user to merge their translations 238 | into "messages" domain. 239 | 240 | Flask-BabelEx allows extension developers to specify which translation domain to 241 | use:: 242 | 243 | from flask_babelex import Domain 244 | 245 | mydomain = Domain(domain='myext') 246 | 247 | mydomain.lazy_gettext('Hello World!') 248 | 249 | :class:`Domain` contains all gettext-related methods (:meth:`~Domain.gettext`, 250 | :meth:`~Domain.ngettext`, etc). 251 | 252 | In previous example, localizations will be read from the ``myext.mo`` files, but 253 | they have to be located in ``translations`` directory under users Flask application. 254 | If extension is distributed with the localizations, it is possible to specify 255 | their location:: 256 | 257 | from flask_babelex import Domain 258 | 259 | from flask.ext.myext import translations 260 | mydomain = Domain(translations.__path__[0]) 261 | 262 | ``mydomain`` will look for translations in extension directory with default (messages) 263 | domain. 264 | 265 | It is also possible to change the translation domain used by default, 266 | either for each app or per request. 267 | 268 | To set the :class:`Domain` that will be used in an app, pass it to 269 | :class:`Babel` on initialization:: 270 | 271 | from flask import Flask 272 | from flask_babelex import Babel, Domain 273 | 274 | app = Flask(__name__) 275 | domain = Domain(domain='myext') 276 | babel = Babel(app, default_domain=domain) 277 | 278 | Translations will then come from the ``myext.mo`` files by default. 279 | 280 | To change the default domain in a request context, call the 281 | :meth:`~Domain.as_default` method from within the request context:: 282 | 283 | from flask import Flask 284 | from flask_babelex import Babel, Domain, gettext 285 | 286 | app = Flask(__name__) 287 | domain = Domain(domain='myext') 288 | babel = Babel(app) 289 | 290 | @app.route('/path') 291 | def demopage(): 292 | domain.as_default() 293 | 294 | return gettext('Hello World!') 295 | 296 | ``Hello World!`` will get translated using the ``myext.mo`` files, but 297 | other requests will use the default ``messages.mo``. Note that a 298 | :class:`Babel` must be initialized for the app for translations to 299 | work at all. 300 | 301 | Troubleshooting 302 | --------------- 303 | 304 | On Snow Leopard pybabel will most likely fail with an exception. If this 305 | happens, check if this command outputs UTF-8:: 306 | 307 | $ echo $LC_CTYPE 308 | UTF-8 309 | 310 | This is a OS X bug unfortunately. To fix it, put the following lines into 311 | your ``~/.profile`` file:: 312 | 313 | export LC_CTYPE=en_US.utf-8 314 | 315 | Then restart your terminal. 316 | 317 | API 318 | --- 319 | 320 | This part of the documentation documents each and every public class or 321 | function from Flask-BabelEx. 322 | 323 | Configuration 324 | ````````````` 325 | 326 | .. autoclass:: Babel 327 | :members: 328 | 329 | Context Functions 330 | ````````````````` 331 | 332 | .. autofunction:: get_locale 333 | 334 | .. autofunction:: get_timezone 335 | 336 | Translation domains 337 | ``````````````````` 338 | 339 | .. autoclass:: Domain 340 | :members: 341 | 342 | Datetime Functions 343 | `````````````````` 344 | 345 | .. autofunction:: to_user_timezone 346 | 347 | .. autofunction:: to_utc 348 | 349 | .. autofunction:: format_datetime 350 | 351 | .. autofunction:: format_date 352 | 353 | .. autofunction:: format_time 354 | 355 | .. autofunction:: format_timedelta 356 | 357 | Gettext Functions 358 | ````````````````` 359 | 360 | .. autofunction:: gettext 361 | 362 | .. autofunction:: ngettext 363 | 364 | .. autofunction:: pgettext 365 | 366 | .. autofunction:: npgettext 367 | 368 | .. autofunction:: lazy_gettext 369 | 370 | .. autofunction:: lazy_pgettext 371 | 372 | Low-Level API 373 | ````````````` 374 | 375 | .. autofunction:: refresh 376 | 377 | 378 | .. _Flask: http://flask.pocoo.org/ 379 | .. _babel: http://babel.edgewall.org/ 380 | .. _pytz: http://pytz.sourceforge.net/ 381 | .. _speaklater: http://pypi.python.org/pypi/speaklater 382 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FlaskBabel.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FlaskBabel.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /flask_babelex/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_babelex 4 | ~~~~~~~~~~~~~ 5 | 6 | Implements i18n/l10n support for Flask applications based on Babel. 7 | 8 | :copyright: (c) 2013 by Serge S. Koval, Armin Ronacher and contributors. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | from __future__ import absolute_import 12 | import os 13 | 14 | # this is a workaround for a snow leopard bug that babel does not 15 | # work around :) 16 | if os.environ.get('LC_CTYPE', '').lower() == 'utf-8': 17 | os.environ['LC_CTYPE'] = 'en_US.utf-8' 18 | 19 | from datetime import datetime 20 | from flask import _request_ctx_stack 21 | from babel import dates, numbers, support, Locale 22 | from babel.support import NullTranslations 23 | from werkzeug.datastructures import ImmutableDict 24 | try: 25 | from pytz.gae import pytz 26 | except ImportError: 27 | from pytz import timezone, UTC 28 | else: 29 | timezone = pytz.timezone 30 | UTC = pytz.UTC 31 | 32 | from flask_babelex._compat import string_types 33 | 34 | _DEFAULT_LOCALE = Locale.parse('en') 35 | 36 | 37 | class Babel(object): 38 | """Central controller class that can be used to configure how 39 | Flask-Babel behaves. Each application that wants to use Flask-Babel 40 | has to create, or run :meth:`init_app` on, an instance of this class 41 | after the configuration was initialized. 42 | """ 43 | 44 | default_date_formats = ImmutableDict({ 45 | 'time': 'medium', 46 | 'date': 'medium', 47 | 'datetime': 'medium', 48 | 'time.short': None, 49 | 'time.medium': None, 50 | 'time.full': None, 51 | 'time.long': None, 52 | 'date.short': None, 53 | 'date.medium': None, 54 | 'date.full': None, 55 | 'date.long': None, 56 | 'datetime.short': None, 57 | 'datetime.medium': None, 58 | 'datetime.full': None, 59 | 'datetime.long': None, 60 | }) 61 | 62 | def __init__(self, app=None, default_locale='en', default_timezone='UTC', 63 | date_formats=None, configure_jinja=True, default_domain=None): 64 | self._default_locale = default_locale 65 | self._default_timezone = default_timezone 66 | self._date_formats = date_formats 67 | self._configure_jinja = configure_jinja 68 | self.app = app 69 | 70 | self._locale_cache = dict() 71 | 72 | if default_domain is None: 73 | self._default_domain = Domain() 74 | else: 75 | self._default_domain = default_domain 76 | 77 | self.locale_selector_func = None 78 | self.timezone_selector_func = None 79 | 80 | if app is not None: 81 | self.init_app(app) 82 | 83 | def init_app(self, app): 84 | """Set up this instance for use with *app*, if no app was passed to 85 | the constructor. 86 | """ 87 | self.app = app 88 | app.babel_instance = self 89 | if not hasattr(app, 'extensions'): 90 | app.extensions = {} 91 | app.extensions['babel'] = self 92 | 93 | app.config.setdefault('BABEL_DEFAULT_LOCALE', self._default_locale) 94 | app.config.setdefault('BABEL_DEFAULT_TIMEZONE', self._default_timezone) 95 | if self._date_formats is None: 96 | self._date_formats = self.default_date_formats.copy() 97 | 98 | #: a mapping of Babel datetime format strings that can be modified 99 | #: to change the defaults. If you invoke :func:`format_datetime` 100 | #: and do not provide any format string Flask-Babel will do the 101 | #: following things: 102 | #: 103 | #: 1. look up ``date_formats['datetime']``. By default ``'medium'`` 104 | #: is returned to enforce medium length datetime formats. 105 | #: 2. ``date_formats['datetime.medium'] (if ``'medium'`` was 106 | #: returned in step one) is looked up. If the return value 107 | #: is anything but `None` this is used as new format string. 108 | #: otherwise the default for that language is used. 109 | self.date_formats = self._date_formats 110 | 111 | if self._configure_jinja: 112 | app.jinja_env.filters.update( 113 | datetimeformat=format_datetime, 114 | dateformat=format_date, 115 | timeformat=format_time, 116 | timedeltaformat=format_timedelta, 117 | 118 | numberformat=format_number, 119 | decimalformat=format_decimal, 120 | currencyformat=format_currency, 121 | percentformat=format_percent, 122 | scientificformat=format_scientific, 123 | ) 124 | app.jinja_env.add_extension('jinja2.ext.i18n') 125 | app.jinja_env.install_gettext_callables( 126 | lambda x: get_domain().get_translations().ugettext(x), 127 | lambda s, p, n: get_domain().get_translations().ungettext(s, p, n), 128 | newstyle=True 129 | ) 130 | 131 | def localeselector(self, f): 132 | """Registers a callback function for locale selection. The default 133 | behaves as if a function was registered that returns `None` all the 134 | time. If `None` is returned, the locale falls back to the one from 135 | the configuration. 136 | 137 | This has to return the locale as string (eg: ``'de_AT'``, ''`en_US`'') 138 | """ 139 | assert self.locale_selector_func is None, \ 140 | 'a localeselector function is already registered' 141 | self.locale_selector_func = f 142 | return f 143 | 144 | def timezoneselector(self, f): 145 | """Registers a callback function for timezone selection. The default 146 | behaves as if a function was registered that returns `None` all the 147 | time. If `None` is returned, the timezone falls back to the one from 148 | the configuration. 149 | 150 | This has to return the timezone as string (eg: ``'Europe/Vienna'``) 151 | """ 152 | assert self.timezone_selector_func is None, \ 153 | 'a timezoneselector function is already registered' 154 | self.timezone_selector_func = f 155 | return f 156 | 157 | def list_translations(self): 158 | """Returns a list of all the locales translations exist for. The 159 | list returned will be filled with actual locale objects and not just 160 | strings. 161 | 162 | .. versionadded:: 0.6 163 | """ 164 | dirname = os.path.join(self.app.root_path, 'translations') 165 | if not os.path.isdir(dirname): 166 | return [] 167 | result = [] 168 | for folder in os.listdir(dirname): 169 | locale_dir = os.path.join(dirname, folder, 'LC_MESSAGES') 170 | if not os.path.isdir(locale_dir): 171 | continue 172 | if filter(lambda x: x.endswith('.mo'), os.listdir(locale_dir)): 173 | result.append(Locale.parse(folder)) 174 | if not result: 175 | result.append(Locale.parse(self._default_locale)) 176 | return result 177 | 178 | @property 179 | def default_locale(self): 180 | """The default locale from the configuration as instance of a 181 | `babel.Locale` object. 182 | """ 183 | return self.load_locale(self.app.config['BABEL_DEFAULT_LOCALE']) 184 | 185 | @property 186 | def default_timezone(self): 187 | """The default timezone from the configuration as instance of a 188 | `pytz.timezone` object. 189 | """ 190 | return timezone(self.app.config['BABEL_DEFAULT_TIMEZONE']) 191 | 192 | def load_locale(self, locale): 193 | """Load locale by name and cache it. Returns instance of a `babel.Locale` 194 | object. 195 | """ 196 | rv = self._locale_cache.get(locale) 197 | if rv is None: 198 | self._locale_cache[locale] = rv = Locale.parse(locale) 199 | return rv 200 | 201 | 202 | def get_locale(): 203 | """Returns the locale that should be used for this request as 204 | `babel.Locale` object. This returns `None` if used outside of 205 | a request. If flask-babel was not attached to the Flask application, 206 | will return 'en' locale. 207 | """ 208 | ctx = _request_ctx_stack.top 209 | if ctx is None: 210 | return None 211 | 212 | locale = getattr(ctx, 'babel_locale', None) 213 | if locale is None: 214 | babel = ctx.app.extensions.get('babel') 215 | 216 | if babel is None: 217 | locale = _DEFAULT_LOCALE 218 | else: 219 | if babel.locale_selector_func is not None: 220 | rv = babel.locale_selector_func() 221 | if rv is None: 222 | locale = babel.default_locale 223 | else: 224 | locale = babel.load_locale(rv) 225 | else: 226 | locale = babel.default_locale 227 | 228 | ctx.babel_locale = locale 229 | 230 | return locale 231 | 232 | 233 | def get_timezone(): 234 | """Returns the timezone that should be used for this request as 235 | `pytz.timezone` object. This returns `None` if used outside of 236 | a request. If flask-babel was not attached to application, will 237 | return UTC timezone object. 238 | """ 239 | ctx = _request_ctx_stack.top 240 | tzinfo = getattr(ctx, 'babel_tzinfo', None) 241 | 242 | if tzinfo is None: 243 | babel = ctx.app.extensions.get('babel') 244 | 245 | if babel is None: 246 | tzinfo = UTC 247 | else: 248 | if babel.timezone_selector_func is None: 249 | tzinfo = babel.default_timezone 250 | else: 251 | rv = babel.timezone_selector_func() 252 | if rv is None: 253 | tzinfo = babel.default_timezone 254 | else: 255 | if isinstance(rv, string_types): 256 | tzinfo = timezone(rv) 257 | else: 258 | tzinfo = rv 259 | 260 | ctx.babel_tzinfo = tzinfo 261 | 262 | return tzinfo 263 | 264 | 265 | def refresh(): 266 | """Refreshes the cached timezones and locale information. This can 267 | be used to switch a translation between a request and if you want 268 | the changes to take place immediately, not just with the next request:: 269 | 270 | user.timezone = request.form['timezone'] 271 | user.locale = request.form['locale'] 272 | refresh() 273 | flash(gettext('Language was changed')) 274 | 275 | Without that refresh, the :func:`~flask.flash` function would probably 276 | return English text and a now German page. 277 | """ 278 | ctx = _request_ctx_stack.top 279 | for key in 'babel_locale', 'babel_tzinfo': 280 | if hasattr(ctx, key): 281 | delattr(ctx, key) 282 | 283 | 284 | def _get_format(key, format): 285 | """A small helper for the datetime formatting functions. Looks up 286 | format defaults for different kinds. 287 | """ 288 | babel = _request_ctx_stack.top.app.extensions.get('babel') 289 | 290 | if babel is not None: 291 | formats = babel.date_formats 292 | else: 293 | formats = Babel.default_date_formats 294 | 295 | if format is None: 296 | format = formats[key] 297 | if format in ('short', 'medium', 'full', 'long'): 298 | rv = formats['%s.%s' % (key, format)] 299 | if rv is not None: 300 | format = rv 301 | return format 302 | 303 | 304 | def to_user_timezone(datetime): 305 | """Convert a datetime object to the user's timezone. This automatically 306 | happens on all date formatting unless rebasing is disabled. If you need 307 | to convert a :class:`datetime.datetime` object at any time to the user's 308 | timezone (as returned by :func:`get_timezone` this function can be used). 309 | """ 310 | if datetime.tzinfo is None: 311 | datetime = datetime.replace(tzinfo=UTC) 312 | tzinfo = get_timezone() 313 | return tzinfo.normalize(datetime.astimezone(tzinfo)) 314 | 315 | 316 | def to_utc(datetime): 317 | """Convert a datetime object to UTC and drop tzinfo. This is the 318 | opposite operation to :func:`to_user_timezone`. 319 | """ 320 | if datetime.tzinfo is None: 321 | datetime = get_timezone().localize(datetime) 322 | return datetime.astimezone(UTC).replace(tzinfo=None) 323 | 324 | 325 | def format_datetime(datetime=None, format=None, rebase=True): 326 | """Return a date formatted according to the given pattern. If no 327 | :class:`~datetime.datetime` object is passed, the current time is 328 | assumed. By default rebasing happens which causes the object to 329 | be converted to the users's timezone (as returned by 330 | :func:`to_user_timezone`). This function formats both date and 331 | time. 332 | 333 | The format parameter can either be ``'short'``, ``'medium'``, 334 | ``'long'`` or ``'full'`` (in which cause the language's default for 335 | that setting is used, or the default from the :attr:`Babel.date_formats` 336 | mapping is used) or a format string as documented by Babel. 337 | 338 | This function is also available in the template context as filter 339 | named `datetimeformat`. 340 | """ 341 | format = _get_format('datetime', format) 342 | return _date_format(dates.format_datetime, datetime, format, rebase) 343 | 344 | 345 | def format_date(date=None, format=None, rebase=True): 346 | """Return a date formatted according to the given pattern. If no 347 | :class:`~datetime.datetime` or :class:`~datetime.date` object is passed, 348 | the current time is assumed. By default rebasing happens which causes 349 | the object to be converted to the users's timezone (as returned by 350 | :func:`to_user_timezone`). This function only formats the date part 351 | of a :class:`~datetime.datetime` object. 352 | 353 | The format parameter can either be ``'short'``, ``'medium'``, 354 | ``'long'`` or ``'full'`` (in which cause the language's default for 355 | that setting is used, or the default from the :attr:`Babel.date_formats` 356 | mapping is used) or a format string as documented by Babel. 357 | 358 | This function is also available in the template context as filter 359 | named `dateformat`. 360 | """ 361 | if rebase and isinstance(date, datetime): 362 | date = to_user_timezone(date) 363 | format = _get_format('date', format) 364 | return _date_format(dates.format_date, date, format, rebase) 365 | 366 | 367 | def format_time(time=None, format=None, rebase=True): 368 | """Return a time formatted according to the given pattern. If no 369 | :class:`~datetime.datetime` object is passed, the current time is 370 | assumed. By default rebasing happens which causes the object to 371 | be converted to the users's timezone (as returned by 372 | :func:`to_user_timezone`). This function formats both date and 373 | time. 374 | 375 | The format parameter can either be ``'short'``, ``'medium'``, 376 | ``'long'`` or ``'full'`` (in which cause the language's default for 377 | that setting is used, or the default from the :attr:`Babel.date_formats` 378 | mapping is used) or a format string as documented by Babel. 379 | 380 | This function is also available in the template context as filter 381 | named `timeformat`. 382 | """ 383 | format = _get_format('time', format) 384 | return _date_format(dates.format_time, time, format, rebase) 385 | 386 | 387 | def format_timedelta(datetime_or_timedelta, granularity='second'): 388 | """Format the elapsed time from the given date to now or the given 389 | timedelta. This currently requires an unreleased development 390 | version of Babel. 391 | 392 | This function is also available in the template context as filter 393 | named `timedeltaformat`. 394 | """ 395 | if isinstance(datetime_or_timedelta, datetime): 396 | datetime_or_timedelta = datetime.utcnow() - datetime_or_timedelta 397 | return dates.format_timedelta(datetime_or_timedelta, granularity, 398 | locale=get_locale()) 399 | 400 | 401 | def _date_format(formatter, obj, format, rebase, **extra): 402 | """Internal helper that formats the date.""" 403 | locale = get_locale() 404 | extra = {} 405 | if formatter is not dates.format_date and rebase: 406 | extra['tzinfo'] = get_timezone() 407 | return formatter(obj, format, locale=locale, **extra) 408 | 409 | 410 | def format_number(number): 411 | """Return the given number formatted for the locale in request 412 | 413 | :param number: the number to format 414 | :return: the formatted number 415 | :rtype: unicode 416 | """ 417 | locale = get_locale() 418 | return numbers.format_number(number, locale=locale) 419 | 420 | 421 | def format_decimal(number, format=None): 422 | """Return the given decimal number formatted for the locale in request 423 | 424 | :param number: the number to format 425 | :param format: the format to use 426 | :return: the formatted number 427 | :rtype: unicode 428 | """ 429 | locale = get_locale() 430 | return numbers.format_decimal(number, format=format, locale=locale) 431 | 432 | 433 | def format_currency(number, currency, format=None): 434 | """Return the given number formatted for the locale in request 435 | 436 | :param number: the number to format 437 | :param currency: the currency code 438 | :param format: the format to use 439 | :return: the formatted number 440 | :rtype: unicode 441 | """ 442 | locale = get_locale() 443 | return numbers.format_currency( 444 | number, currency, format=format, locale=locale 445 | ) 446 | 447 | 448 | def format_percent(number, format=None): 449 | """Return formatted percent value for the locale in request 450 | 451 | :param number: the number to format 452 | :param format: the format to use 453 | :return: the formatted percent number 454 | :rtype: unicode 455 | """ 456 | locale = get_locale() 457 | return numbers.format_percent(number, format=format, locale=locale) 458 | 459 | 460 | def format_scientific(number, format=None): 461 | """Return value formatted in scientific notation for the locale in request 462 | 463 | :param number: the number to format 464 | :param format: the format to use 465 | :return: the formatted percent number 466 | :rtype: unicode 467 | """ 468 | locale = get_locale() 469 | return numbers.format_scientific(number, format=format, locale=locale) 470 | 471 | 472 | class Domain(object): 473 | """Localization domain. By default will use look for tranlations in Flask application directory 474 | and "messages" domain - all message catalogs should be called ``messages.mo``. 475 | """ 476 | def __init__(self, dirname=None, domain='messages'): 477 | self.dirname = dirname 478 | self.domain = domain 479 | 480 | self.cache = dict() 481 | 482 | def as_default(self): 483 | """Set this domain as default for the current request""" 484 | ctx = _request_ctx_stack.top 485 | if ctx is None: 486 | raise RuntimeError("No request context") 487 | 488 | ctx.babel_domain = self 489 | 490 | def get_translations_cache(self, ctx): 491 | """Returns dictionary-like object for translation caching""" 492 | return self.cache 493 | 494 | def get_translations_path(self, ctx): 495 | """Returns translations directory path. Override if you want 496 | to implement custom behavior. 497 | """ 498 | return self.dirname or os.path.join(ctx.app.root_path, 'translations') 499 | 500 | def get_translations(self): 501 | """Returns the correct gettext translations that should be used for 502 | this request. This will never fail and return a dummy translation 503 | object if used outside of the request or if a translation cannot be 504 | found. 505 | """ 506 | ctx = _request_ctx_stack.top 507 | if ctx is None: 508 | return NullTranslations() 509 | 510 | locale = get_locale() 511 | 512 | cache = self.get_translations_cache(ctx) 513 | 514 | translations = cache.get(str(locale)) 515 | if translations is None: 516 | dirname = self.get_translations_path(ctx) 517 | translations = support.Translations.load(dirname, 518 | locale, 519 | domain=self.domain) 520 | cache[str(locale)] = translations 521 | 522 | return translations 523 | 524 | def gettext(self, string, **variables): 525 | """Translates a string with the current locale and passes in the 526 | given keyword arguments as mapping to a string formatting string. 527 | 528 | :: 529 | 530 | gettext(u'Hello World!') 531 | gettext(u'Hello %(name)s!', name='World') 532 | """ 533 | t = self.get_translations() 534 | if variables: 535 | return t.ugettext(string) % variables 536 | else: 537 | return t.ugettext(string) 538 | 539 | def ngettext(self, singular, plural, num, **variables): 540 | """Translates a string with the current locale and passes in the 541 | given keyword arguments as mapping to a string formatting string. 542 | The `num` parameter is used to dispatch between singular and various 543 | plural forms of the message. It is available in the format string 544 | as ``%(num)d`` or ``%(num)s``. The source language should be 545 | English or a similar language which only has one plural form. 546 | 547 | :: 548 | 549 | ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples)) 550 | """ 551 | variables.setdefault('num', num) 552 | t = self.get_translations() 553 | return t.ungettext(singular, plural, num) % variables 554 | 555 | def pgettext(self, context, string, **variables): 556 | """Like :func:`gettext` but with a context. 557 | 558 | .. versionadded:: 0.7 559 | """ 560 | t = self.get_translations() 561 | return t.upgettext(context, string) % variables 562 | 563 | def npgettext(self, context, singular, plural, num, **variables): 564 | """Like :func:`ngettext` but with a context. 565 | 566 | .. versionadded:: 0.7 567 | """ 568 | variables.setdefault('num', num) 569 | t = self.get_translations() 570 | return t.unpgettext(context, singular, plural, num) % variables 571 | 572 | def lazy_gettext(self, string, **variables): 573 | """Like :func:`gettext` but the string returned is lazy which means 574 | it will be translated when it is used as an actual string. 575 | 576 | Example:: 577 | 578 | hello = lazy_gettext(u'Hello World') 579 | 580 | @app.route('/') 581 | def index(): 582 | return unicode(hello) 583 | """ 584 | from speaklater import make_lazy_string 585 | return make_lazy_string(self.gettext, string, **variables) 586 | 587 | def lazy_pgettext(self, context, string, **variables): 588 | """Like :func:`pgettext` but the string returned is lazy which means 589 | it will be translated when it is used as an actual string. 590 | 591 | .. versionadded:: 0.7 592 | """ 593 | from speaklater import make_lazy_string 594 | return make_lazy_string(self.pgettext, context, string, **variables) 595 | 596 | # This is the domain that will be used if there is no request context (and thus no app) 597 | # or if the app isn't initialized for babel. Note that if there is no request context, 598 | # then the standard Domain will use NullTranslations 599 | domain = Domain() 600 | 601 | def get_domain(): 602 | """Return the correct translation domain that is used for this request. 603 | This will return the default domain (e.g. "messages" in /translations") 604 | if none is set for this request. 605 | """ 606 | ctx = _request_ctx_stack.top 607 | if ctx is None: 608 | return domain 609 | 610 | try: 611 | return ctx.babel_domain 612 | except AttributeError: 613 | pass 614 | 615 | babel = ctx.app.extensions.get('babel') 616 | if babel is not None: 617 | d = babel._default_domain 618 | else: 619 | d = domain 620 | 621 | ctx.babel_domain = d 622 | return d 623 | 624 | # Create shortcuts for the default Flask domain 625 | def gettext(*args, **kwargs): 626 | return get_domain().gettext(*args, **kwargs) 627 | _ = gettext 628 | def ngettext(*args, **kwargs): 629 | return get_domain().ngettext(*args, **kwargs) 630 | def pgettext(*args, **kwargs): 631 | return get_domain().pgettext(*args, **kwargs) 632 | def npgettext(*args, **kwargs): 633 | return get_domain().npgettext(*args, **kwargs) 634 | def lazy_gettext(*args, **kwargs): 635 | from speaklater import make_lazy_string 636 | return make_lazy_string(gettext, *args, **kwargs) 637 | def lazy_pgettext(*args, **kwargs): 638 | from speaklater import make_lazy_string 639 | return make_lazy_string(pgettext, *args, **kwargs) 640 | -------------------------------------------------------------------------------- /flask_babelex/_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_babelex._compat 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :copyright: (c) 2013 by Armin Ronacher, Daniel Neuhäuser. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | import sys 10 | 11 | 12 | PY2 = sys.version_info[0] == 2 13 | 14 | 15 | if PY2: 16 | text_type = unicode 17 | string_types = (str, unicode) 18 | else: 19 | text_type = str 20 | string_types = (str, ) 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [upload_docs] 2 | upload-dir = docs/_build/html 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-BabelEx 3 | ------------- 4 | 5 | Adds i18n/l10n support to Flask applications with the help of the 6 | `Babel`_ library. 7 | 8 | This is fork of official Flask-Babel extension with following features: 9 | 10 | 1. It is possible to use multiple language catalogs in one Flask application; 11 | 2. Localization domains: your extension can package localization file(s) and use them 12 | if necessary; 13 | 3. Does not reload localizations for each request. 14 | 15 | Links 16 | ````` 17 | 18 | * `documentation `_ 19 | * `development version 20 | `_ 21 | * `original Flask-Babel extension `_. 22 | 23 | .. _Babel: http://babel.edgewall.org/ 24 | 25 | """ 26 | from setuptools import setup 27 | 28 | 29 | setup( 30 | name='Flask-BabelEx', 31 | version='0.9.4', 32 | url='http://github.com/mrjoes/flask-babelex', 33 | license='BSD', 34 | author='Serge S. Koval', 35 | author_email='serge.koval+github@gmail.com', 36 | description='Adds i18n/l10n support to Flask applications', 37 | long_description=__doc__, 38 | packages=['flask_babelex'], 39 | zip_safe=False, 40 | platforms='any', 41 | install_requires=[ 42 | 'Flask', 43 | 'Babel>=1.0', 44 | 'speaklater>=1.2', 45 | 'Jinja2>=2.5' 46 | ], 47 | classifiers=[ 48 | 'Development Status :: 4 - Beta', 49 | 'Environment :: Web Environment', 50 | 'Intended Audience :: Developers', 51 | 'License :: OSI Approved :: BSD License', 52 | 'Operating System :: OS Independent', 53 | 'Programming Language :: Python', 54 | 'Programming Language :: Python :: 3', 55 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 56 | 'Topic :: Software Development :: Libraries :: Python Modules' 57 | ] 58 | ) 59 | -------------------------------------------------------------------------------- /tests/babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import with_statement 3 | 4 | import sys 5 | import os 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 7 | 8 | import unittest 9 | from decimal import Decimal 10 | import flask 11 | from datetime import datetime 12 | import flask_babelex as babel 13 | from flask_babelex import gettext, ngettext, lazy_gettext 14 | from flask_babelex._compat import text_type 15 | 16 | 17 | class DateFormattingTestCase(unittest.TestCase): 18 | 19 | def test_basics(self): 20 | app = flask.Flask(__name__) 21 | b = babel.Babel(app) 22 | d = datetime(2010, 4, 12, 13, 46) 23 | 24 | with app.test_request_context(): 25 | assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' 26 | assert babel.format_date(d) == 'Apr 12, 2010' 27 | assert babel.format_time(d) == '1:46:00 PM' 28 | 29 | with app.test_request_context(): 30 | app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' 31 | assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' 32 | assert babel.format_date(d) == 'Apr 12, 2010' 33 | assert babel.format_time(d) == '3:46:00 PM' 34 | 35 | with app.test_request_context(): 36 | app.config['BABEL_DEFAULT_LOCALE'] = 'de_DE' 37 | assert babel.format_datetime(d, 'long') == \ 38 | '12. April 2010 15:46:00 MESZ' 39 | 40 | def test_init_app(self): 41 | b = babel.Babel() 42 | app = flask.Flask(__name__) 43 | b.init_app(app) 44 | d = datetime(2010, 4, 12, 13, 46) 45 | 46 | with app.test_request_context(): 47 | assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' 48 | assert babel.format_date(d) == 'Apr 12, 2010' 49 | assert babel.format_time(d) == '1:46:00 PM' 50 | 51 | with app.test_request_context(): 52 | app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' 53 | assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' 54 | assert babel.format_date(d) == 'Apr 12, 2010' 55 | assert babel.format_time(d) == '3:46:00 PM' 56 | 57 | with app.test_request_context(): 58 | app.config['BABEL_DEFAULT_LOCALE'] = 'de_DE' 59 | assert babel.format_datetime(d, 'long') == \ 60 | '12. April 2010 15:46:00 MESZ' 61 | 62 | def test_custom_formats(self): 63 | app = flask.Flask(__name__) 64 | app.config.update( 65 | BABEL_DEFAULT_LOCALE='en_US', 66 | BABEL_DEFAULT_TIMEZONE='Pacific/Johnston' 67 | ) 68 | b = babel.Babel(app) 69 | b.date_formats['datetime'] = 'long' 70 | b.date_formats['datetime.long'] = 'MMMM d, yyyy h:mm:ss a' 71 | d = datetime(2010, 4, 12, 13, 46) 72 | 73 | with app.test_request_context(): 74 | assert babel.format_datetime(d) == 'April 12, 2010 3:46:00 AM' 75 | 76 | def test_custom_locale_selector(self): 77 | app = flask.Flask(__name__) 78 | b = babel.Babel(app) 79 | d = datetime(2010, 4, 12, 13, 46) 80 | 81 | the_timezone = 'UTC' 82 | the_locale = 'en_US' 83 | 84 | @b.localeselector 85 | def select_locale(): 86 | return the_locale 87 | @b.timezoneselector 88 | def select_timezone(): 89 | return the_timezone 90 | 91 | with app.test_request_context(): 92 | assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' 93 | 94 | the_locale = 'de_DE' 95 | the_timezone = 'Europe/Vienna' 96 | 97 | with app.test_request_context(): 98 | assert babel.format_datetime(d) == '12.04.2010 15:46:00' 99 | 100 | def test_refreshing(self): 101 | app = flask.Flask(__name__) 102 | b = babel.Babel(app) 103 | d = datetime(2010, 4, 12, 13, 46) 104 | with app.test_request_context(): 105 | assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' 106 | app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' 107 | babel.refresh() 108 | assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' 109 | 110 | def test_non_initialized(self): 111 | app = flask.Flask(__name__) 112 | d = datetime(2010, 4, 12, 13, 46) 113 | with app.test_request_context(): 114 | assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' 115 | 116 | 117 | class NumberFormattingTestCase(unittest.TestCase): 118 | 119 | def test_basics(self): 120 | app = flask.Flask(__name__) 121 | b = babel.Babel(app) 122 | n = 1099 123 | 124 | with app.test_request_context(): 125 | assert babel.format_number(n) == u'1,099' 126 | assert babel.format_decimal(Decimal('1010.99')) == u'1,010.99' 127 | assert babel.format_currency(n, 'USD') == '$1,099.00' 128 | assert babel.format_percent(0.19) == '19%' 129 | assert babel.format_scientific(10000) == u'1E4' 130 | 131 | 132 | class GettextTestCase(unittest.TestCase): 133 | 134 | def test_basics(self): 135 | app = flask.Flask(__name__) 136 | b = babel.Babel(app, default_locale='de_DE') 137 | 138 | with app.test_request_context(): 139 | assert gettext(u'Hello %(name)s!', name='Peter') == 'Hallo Peter!' 140 | assert ngettext(u'%(num)s Apple', u'%(num)s Apples', 3) == u'3 Äpfel' 141 | assert ngettext(u'%(num)s Apple', u'%(num)s Apples', 1) == u'1 Apfel' 142 | 143 | def test_template_basics(self): 144 | app = flask.Flask(__name__) 145 | b = babel.Babel(app, default_locale='de_DE') 146 | 147 | t = lambda x: flask.render_template_string('{{ %s }}' % x) 148 | 149 | with app.test_request_context(): 150 | assert t("gettext('Hello %(name)s!', name='Peter')") == 'Hallo Peter!' 151 | assert t("ngettext('%(num)s Apple', '%(num)s Apples', 3)") == u'3 Äpfel' 152 | assert t("ngettext('%(num)s Apple', '%(num)s Apples', 1)") == u'1 Apfel' 153 | assert flask.render_template_string(''' 154 | {% trans %}Hello {{ name }}!{% endtrans %} 155 | ''', name='Peter').strip() == 'Hallo Peter!' 156 | assert flask.render_template_string(''' 157 | {% trans num=3 %}{{ num }} Apple 158 | {%- pluralize %}{{ num }} Apples{% endtrans %} 159 | ''', name='Peter').strip() == u'3 Äpfel' 160 | 161 | def test_lazy_gettext(self): 162 | app = flask.Flask(__name__) 163 | b = babel.Babel(app, default_locale='de_DE') 164 | yes = lazy_gettext(u'Yes') 165 | with app.test_request_context(): 166 | assert text_type(yes) == 'Ja' 167 | app.config['BABEL_DEFAULT_LOCALE'] = 'en_US' 168 | with app.test_request_context(): 169 | assert text_type(yes) == 'Yes' 170 | 171 | def test_lazy_gettext_defaultdomain(self): 172 | app = flask.Flask(__name__) 173 | domain = babel.Domain(domain='test') 174 | b = babel.Babel(app, default_locale='de_DE', default_domain=domain) 175 | first = lazy_gettext('first') 176 | with app.test_request_context(): 177 | assert text_type(first) == 'erste' 178 | app.config['BABEL_DEFAULT_LOCALE'] = 'en_US' 179 | with app.test_request_context(): 180 | assert text_type(first) == 'first' 181 | 182 | def test_list_translations(self): 183 | app = flask.Flask(__name__) 184 | b = babel.Babel(app, default_locale='de_DE') 185 | translations = b.list_translations() 186 | assert len(translations) == 1 187 | assert str(translations[0]) == 'de' 188 | 189 | def test_domain(self): 190 | app = flask.Flask(__name__) 191 | b = babel.Babel(app, default_locale='de_DE') 192 | domain = babel.Domain(domain='test') 193 | 194 | with app.test_request_context(): 195 | assert domain.gettext('first') == 'erste' 196 | assert babel.gettext('first') == 'first' 197 | 198 | def test_as_default(self): 199 | app = flask.Flask(__name__) 200 | b = babel.Babel(app, default_locale='de_DE') 201 | domain = babel.Domain(domain='test') 202 | 203 | with app.test_request_context(): 204 | assert babel.gettext('first') == 'first' 205 | domain.as_default() 206 | assert babel.gettext('first') == 'erste' 207 | 208 | def test_default_domain(self): 209 | app = flask.Flask(__name__) 210 | domain = babel.Domain(domain='test') 211 | b = babel.Babel(app, default_locale='de_DE', default_domain=domain) 212 | 213 | with app.test_request_context(): 214 | assert babel.gettext('first') == 'erste' 215 | 216 | def test_non_initialized(self): 217 | app = flask.Flask(__name__) 218 | with app.test_request_context(): 219 | assert babel.gettext('first') == 'first' 220 | 221 | def test_multiple_apps(self): 222 | app1 = flask.Flask(__name__) 223 | b1 = babel.Babel(app1, default_locale='de_DE') 224 | 225 | app2 = flask.Flask(__name__) 226 | b2 = babel.Babel(app2, default_locale='de_DE') 227 | 228 | with app1.test_request_context(): 229 | assert babel.gettext('Yes') == 'Ja' 230 | 231 | assert 'de_DE' in b1._default_domain.cache 232 | 233 | with app2.test_request_context(): 234 | assert 'de_DE' not in b2._default_domain.cache 235 | 236 | if __name__ == '__main__': 237 | unittest.main() 238 | -------------------------------------------------------------------------------- /tests/translations/de/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjoes/flask-babelex/e395444ff7207e2585217a34ffdf0c5627f7b89b/tests/translations/de/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /tests/translations/de/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # German translations for PROJECT. 2 | # Copyright (C) 2010 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2010. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2010-05-29 17:00+0200\n" 11 | "PO-Revision-Date: 2010-05-30 12:56+0200\n" 12 | "Last-Translator: Armin Ronacher \n" 13 | "Language-Team: de \n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.5\n" 19 | 20 | #: tests.py:94 21 | #, python-format 22 | msgid "Hello %(name)s!" 23 | msgstr "Hallo %(name)s!" 24 | 25 | #: tests.py:95 tests.py:96 26 | #, python-format 27 | msgid "%(num)s Apple" 28 | msgid_plural "%(num)s Apples" 29 | msgstr[0] "%(num)s Apfel" 30 | msgstr[1] "%(num)s Äpfel" 31 | 32 | #: tests.py:119 33 | msgid "Yes" 34 | msgstr "Ja" 35 | 36 | -------------------------------------------------------------------------------- /tests/translations/de/LC_MESSAGES/test.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjoes/flask-babelex/e395444ff7207e2585217a34ffdf0c5627f7b89b/tests/translations/de/LC_MESSAGES/test.mo -------------------------------------------------------------------------------- /tests/translations/de/LC_MESSAGES/test.po: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2010 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2010. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2010-05-30 12:56+0200\n" 11 | "PO-Revision-Date: 2012-04-11 15:18+0200\n" 12 | "Last-Translator: Serge S. Koval \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 0.9.5\n" 18 | 19 | #: tests.py:94 20 | #, python-format 21 | msgid "first" 22 | msgstr "erste" 23 | 24 | -------------------------------------------------------------------------------- /tests/translations/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2010 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2010. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2010-05-30 12:56+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.5\n" 19 | 20 | #: tests.py:94 21 | #, python-format 22 | msgid "Hello %(name)s!" 23 | msgstr "" 24 | 25 | #: tests.py:95 tests.py:96 26 | #, python-format 27 | msgid "%(num)s Apple" 28 | msgid_plural "%(num)s Apples" 29 | msgstr[0] "" 30 | msgstr[1] "" 31 | 32 | #: tests.py:119 33 | msgid "Yes" 34 | msgstr "" 35 | 36 | -------------------------------------------------------------------------------- /tests/translations/test.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2010 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2010. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2010-05-30 12:56+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.5\n" 19 | 20 | #: tests.py:94 21 | #, python-format 22 | msgid "First" 23 | msgstr "" 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py33 3 | 4 | [testenv] 5 | deps = pytz>=2013a 6 | whitelist_externals = make 7 | commands = make test 8 | --------------------------------------------------------------------------------