flufl_i18n-5.1.0/conftest.py 0000644 0000000 0000000 00000002276 13615410400 012612 0 ustar 00 import os
import sys
from sybil import Sybil
from doctest import ELLIPSIS, REPORT_NDIFF, NORMALIZE_WHITESPACE
from contextlib import ExitStack
from sybil.parsers.doctest import DocTestParser
from sybil.parsers.codeblock import PythonCodeBlockParser
# For the message catalog used in the doctests.
from test.data import messages
DOCTEST_FLAGS = ELLIPSIS | NORMALIZE_WHITESPACE | REPORT_NDIFF
class DoctestNamespace:
def __init__(self):
self._resources = ExitStack()
def setup(self, namespace):
sys.modules['messages'] = messages
namespace['cleanups'] = self._resources
# Ensure that environment variables affecting translation are
# neutralized.
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
if envar in os.environ:
del os.environ[envar]
def teardown(self, namespace):
del sys.modules['messages']
self._resources.close()
namespace = DoctestNamespace()
pytest_collect_file = Sybil(
parsers=[
DocTestParser(optionflags=DOCTEST_FLAGS),
PythonCodeBlockParser(),
],
pattern='*.rst',
setup=namespace.setup,
teardown=namespace.teardown,
).pytest()
flufl_i18n-5.1.0/docs/NEWS.rst 0000644 0000000 0000000 00000013111 13615410400 012637 0 ustar 00 =====================
flufl.i18n change log
=====================
5.1 (2024-03-30)
================
* Add support for Python 3.12. (GL#19)
* Switch to ``hatch``, replacing ``pdm`` and ``tox``. (GL#17)
* Switch to ``ruff`` from ``blue`` and ``isort``. (GL#18)
5.0.2 (2023-07-21)
==================
* Update dependencies.
* Other minor improvements and cleanups.
5.0.1 (2023-06-13)
==================
* Fix the build backend.
5.0 (unreleased)
================
* Drop Python 3.7 support. (GL#15)
* Switch from ``flake8`` and ``isort`` to ``ruff`` for code quality. (GL#16)
* Bump dependencies.
4.1.1 (2022-09-05)
==================
* Improvements to the GitLab CI integration.
* Several minor updates to work better with the latest pdm.
4.1 (2022-08-25)
==================
* The standard substitution pattern now ignores the trailing dot on $-string
placeholders. I.e. ``$foo.`` is now recognized as ``$foo``. (GL#12)
* Update to pdm 1.3.
* Update dependencies.
* Make sure the doctest teardown gets run.
* Add support for Python 3.11.
4.0 (2022-01-11)
================
* Use modern package management by adopting `pdm
`_ and ``pyproject.toml``, and dropping ``setup.py``
and ``setup.cfg``.
* Build the docs with Python 3.8.
* Update to version 3.0 of `Sybil `_.
* Adopt the `Furo `_ documentation theme.
* Use `importlib.metadata.version()
`_
as a better way to get the package version number for the documentation.
* Drop Python 3.6 support.
* Update Windows GitLab runner to include Python 3.10.
* Update copyright years.
3.2 (2021-05-29)
================
* Add a ``py.typed`` file to satisfy type checkers. (GL#10)
* Improve some QA by re-adding diff-cover, Gitlab SAST during CI, and testing
on Python 3.10 beta (except for Windows)
* The ``master`` branch is renamed to ``main``. (GL#11)
3.1.5 (2021-02-14)
==================
* I `blue `_ it!
3.1.4 (2021-01-01)
==================
* Update copyright years.
* Include ``test/__init__.py`` and ``docs/__init__.py`` (GL#9)
3.1.3 (2020-10-22)
==================
* Rename top-level tests/ directory to test/ (GL#8)
3.1.2 (2020-10-21)
==================
* Small documentation fix.
3.1.1 (2020-10-21)
==================
* Fix the site-packages pollution. (GL#7)
3.1 (2020-10-20)
================
* Improve the documentation.
* Reorganized docs and tests out of the code directory. (GL#5)
* Fix the Windows CI job. (GL#6)
3.0.1 (2020-07-28)
==================
* Fix pytest 6.0.0 compatibility
* Add CI for Python 3.9 on Windows
3.0 (2020-07-12)
================
* Drop support for Python 3.4 and 3.5. Add support for Python 3.9.
* ``Translator.catalog`` property is now exposed.
* New abstract classes for defining the types in this library:
``TranslationContextManager``, ``RuntimeTranslator``, ``TranslationStrategy``
* When ``expand()`` gets an exception, the original exception is re-raised.
This used to inadvertently return None.
* Add type annotations and API reference documentation.
* Other internal improvements.
2.0.2 (2019-05-17)
==================
* Add (testing) support for Python 3.7 and 3.8.
* Add LICENSE and the top level README.rst file to release tarball. (Closes #4)
2.0.1 (2017-11-14)
==================
* Restore Python 3.4 support.
2.0 (2017-07-24)
================
* Add ``_.defer_translation()`` context manager for marking, but not
translating a string at the point of use. (Closes #2)
* Drop Python 2, 3.3, and 3.4 compatibility; add Python 3.5 and 3.6.
* Switch to the Apache License Version 2.0
* Use flufl.testing for nose2 and flake8 plugins.
1.1.3 (2014-04-25)
==================
* Include MANIFEST.in in the sdist tarball, otherwise the Debian package
won't built correctly.
1.1.2 (2014-03-31)
==================
* Fix documentation bug. LP: #1026403
* Use modern setuptools rather than distutils.
* Bump copyright years.
1.1.1 (2012-04-19)
==================
* Add classifiers to setup.py and make the long description more compatible
with the Cheeseshop.
* Other changes to make the Cheeseshop page look nicer. (LP: #680136)
* setup_helper.py version 2.1.
1.1 (2012-01-19)
================
* Support Python 3 without the need for 2to3.
1.0.4 (2010-12-06)
==================
* Restore missing line from MANIFEST.in to fix distribution tarball.
1.0.3 (2010-12-01)
==================
* Fix setup.py to not install myfixers artifact directory on install.
* Remove pylint.rc; we'll use pyflakes instead.
1.0.2 (2010-06-23)
==================
* Small documentation fix.
1.0.1 (2010-06-09)
==================
* Ditch the use of zc.buildout.
* Improved documentation.
1.0 (2010-04-24)
================
* Use Distribute instead of Setuptools.
* Port to Python 3 when used with 2to3.
* More documentation improvements.
0.6 (2010-04-21)
================
* Documentation and lint clean up.
0.5 (2010-04-20)
================
* Added a simplified initialization API for one-language-context
applications. This works much better for non-server applications.
* Added a SimpleStrategy which recognizes the $LOCPATH environment variable.
* Show how PEP 292 strings are supported automatically.
* When strategies are called with zero arguments, they supply the default
translation context, which is usually a NullTranslation. This is better
than hardcoding the NullTranslation in the Application.
0.4 (2010-03-04)
================
* Add the ability to get the current language code, via _.code
0.3 (2009-11-15)
================
* Initial release.
flufl_i18n-5.1.0/docs/__init__.py 0000644 0000000 0000000 00000000000 13615410400 013433 0 ustar 00 flufl_i18n-5.1.0/docs/apiref.rst 0000644 0000000 0000000 00000001275 13615410400 013341 0 ustar 00 =============
API Reference
=============
.. currentmodule:: flufl.i18n
Classes
=======
.. autoclass:: Application
:members:
.. autoclass:: flufl.i18n._translator.Translator
:members:
.. autoclass:: PackageStrategy
:members:
.. autoclass:: SimpleStrategy
:members:
.. autoclass:: flufl.i18n._registry.Registry
:members:
Functions
=========
.. autofunction:: initialize
.. autofunction:: expand
Globals
=======
See the :class:`~_registry.Registry` class for details.
.. autodata:: registry
:annotation:
Types
=====
.. autoclass:: RuntimeTranslator
:members:
.. autoclass:: TranslationContextManager
:members:
.. autoclass:: TranslationStrategy
:members:
flufl_i18n-5.1.0/docs/conf.py 0000644 0000000 0000000 00000015032 13615410400 012634 0 ustar 00 # -*- coding: utf-8 -*-
#
# flufl.i18n documentation build configuration file, created by
# sphinx-quickstart on Fri Apr 23 11:41:37 2010.
#
# 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 sys, os
from datetime import date
import importlib.metadata
# 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.append(os.path.abspath('../src'))
# -- General configuration -----------------------------------------------------
# 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',
]
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
}
autodoc_typehints = 'both'
autoclass_content = 'both'
# 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'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'flufl.i18n'
author = 'Barry Warsaw'
copyright = f'2009-{date.today().year}, {author}'
# 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 = importlib.metadata.version(project)
# The full version, including alpha/beta/rc tags.
release = version
# 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 documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build', 'build', 'flufl.i18n.egg-info', 'distribute-0.6.10']
# 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. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'furo'
# 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 = []
# 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 = {}
# 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_use_modindex = 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 = 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 = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'flufli18ndoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('README.rst', 'flufli18n.tex', 'flufl.i18n Documentation',
author, 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
flufl_i18n-5.1.0/docs/expand.rst 0000644 0000000 0000000 00000002131 13615410400 013342 0 ustar 00 ===================
Expanding templates
===================
.. currentmodule:: flufl.i18n
The Python standard library defines :class:`string.Template` strings, which
are the implementation of `PEP 292`_ simple string templates. Substitution
placeholders are identified by a leading ``$``-sign [#]_. A substitution
dictionary names the keys and values that should be substituted into the
template, replacing the placeholders. This module defines the
:meth:`expand()` function which takes the translated string and a substitution
dictionary, and returns the resulting string.
Normally though, you shouldn't have to call this function yourself.
::
>>> key_1 = 'key_1'
>>> key_2 = 'key_2'
>>> from flufl.i18n import expand
>>> print(expand(
... '$key_2 is different than $key_1', {
... key_1: 'the first key',
... key_2: 'the second key',
... }))
the second key is different than the first key
.. _`PEP 292`: http://www.python.org/dev/peps/pep-0292/
.. [#] See the :class:`string.Template` documentation for the complete set of
rules.
flufl_i18n-5.1.0/docs/index.rst 0000644 0000000 0000000 00000004377 13615410400 013210 0 ustar 00 ======================================================
flufl.i18n - A high level API for internationalization
======================================================
This package is called ``flufl.i18n``. It provides a high level, convenient
API for managing internationalization translation contexts in Python
application. There is a simple API for single-context applications, such as
command line scripts which only need to translate into one language during the
entire course of their execution. There is a more flexible, but still
convenient API for multi-context applications, such as servers, which may need
to switch language contexts for different tasks.
Requirements
============
``flufl.i18n`` requires Python 3.8 or newer.
Documentation
=============
A `simple guide`_ to using the library is available, along with a detailed
`API reference`_.
Project details
===============
* Project home: https://gitlab.com/warsaw/flufl.i18n
* Report bugs at: https://gitlab.com/warsaw/flufl.i18n/issues
* Code hosting: https://gitlab.com/warsaw/flufl.i18n.git
* Documentation: https://flufli18n.readthedocs.io/
You can install it with ``pip``::
% pip install flufl.i18n
You can grab the latest development copy of the code using git. The master
repository is hosted on GitLab. If you have git installed, you can grab
your own branch of the code like this::
$ git clone https://gitlab.com/warsaw/flufl.i18n.git
You may contact the author via barry@python.org.
Copyright
=========
Copyright (C) 2004-2024 Barry A. Warsaw
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Table of Contents and Index
===========================
* :ref:`genindex`
.. toctree::
:glob:
using
expand
strategies
apiref
NEWS
.. _`simple guide`: using.html
.. _`API reference`: apiref.html
flufl_i18n-5.1.0/docs/strategies.rst 0000644 0000000 0000000 00000006563 13615410400 014252 0 ustar 00 ==================
Catalog strategies
==================
.. currentmodule:: flufl.i18n
The way :doc:`flufl.i18n ` finds its catalog for an application is
extensible. These are called *strategies*. ``flufl.i18n`` comes with a
couple of fairly simple strategies. Usually one of the built-in strategies
will be sufficient, but you can write your own strategies for finding your
catalogs.
Python package strategies
=========================
The :class:`PackageStrategy` class locates catalog files from within an
importable Python package's directory. Inside the package directory, you
still need the `gettext`_ standard filesystem layout of
``/LC_MESSAGES/.mo``.
.. note::
:doc:`As before ` these examples will use the faux ``xx`` language,
and the faux ``messages`` module referred to in these examples contains the
language code directories.
First import the Python package containing the message catalogs for these
examples [#]_.
>>> import messages
By setting the ``$LANG`` environment variable, we can specify that the
application translates into that language automatically.
>>> import os
>>> os.environ['LANG'] = 'xx'
Now we can instantiate a package strategy which finds its catalogs in this
example's ``messages`` Python package. The first argument is the application
name, which must be unique among all registered strategies.
>>> from flufl.i18n import PackageStrategy
>>> strategy = PackageStrategy('flufl', messages)
Once you have the strategy you need, register it with the global registry.
The registration process returns an application object which can be used to
look up language codes.
>>> from flufl.i18n import registry
>>> application = registry.register(strategy)
The application object keeps track of a current translation context, and
exports a method which you can bind to the underscore function in your module
globals for convenient gettext usage.
>>> _ = application._
Now at run time, ``_()`` will always translate its string argument to the
current catalog's language.
>>> print(_('A test message'))
N grfg zrffntr
..
>>> # Hack to unregister the previous strategy.
>>> registry._registry.clear()
Simple strategy
===============
There is also a simpler strategy that uses both the ``$LANG`` environment
variable, and the ``$LOCPATH`` environment variable to set things up.
::
>>> os.environ['LOCPATH'] = os.path.dirname(messages.__file__)
>>> from flufl.i18n import SimpleStrategy
>>> strategy = SimpleStrategy('flufl')
>>> application = registry.register(strategy)
>>> _ = application._
>>> print(_('A test message'))
N grfg zrffntr
Calling with zero arguments
===========================
Strategies should be prepared to accept zero arguments when called, to produce
a *default* translation (usually the :class:`gettext.NullTranslations`).
::
>>> def get_gettext(strategy):
... catalog = strategy()
... return catalog.gettext
>>> print(get_gettext(
... SimpleStrategy('example'))('A test message'))
A test message
>>> print(get_gettext(
... PackageStrategy('example', messages))('A test message'))
A test message
.. _gettext: https://www.gnu.org/software/gettext/manual/gettext.html
.. [#] ``messages`` is not a standard Python package. It exists only for
the examples in this documentation.
flufl_i18n-5.1.0/docs/using.rst 0000644 0000000 0000000 00000025225 13615410400 013221 0 ustar 00 ============================
Using the flufl.i18n library
============================
.. currentmodule:: flufl.i18n
There are two ways that your application can set up translations to use
:doc:`flufl.i18n `. The simple initialization will work for most
applications, where there is only one *language context* for the entire run of
the application, such as for a command line tool. The more complex
initialization works well for applications like servers that may want to use
multiple language contexts during their execution.
Single language contexts
========================
If your applicationonly needs one language context for its entire execution
(such as a command line tool), you can use the simple API to set things up.
>>> from flufl.i18n import initialize
The library by default uses the ``$LANG`` and ``$LOCPATH`` environment
variables to set things up.
As environment variables, ``$LANG`` sets the language code and ``$LOCPATH``
sets the directory containing containing the translation catalogs with the
language codes as subdirectories. Python's :mod:`gettext` module provides
more detail about the underlying organization and technology.
.. note::
In these examples, we're using a faux language with a code of ``xx``.
For demonstration purposes, the ``xx`` language is just the `rot13
`_ of the original source string.
The faux ``messages`` module referred to in these examples contains these
language code directories.
::
>>> import os
>>> # Import the Python package containing all the translations.
>>> import messages
>>> os.environ['LANG'] = 'xx'
>>> os.environ['LOCPATH'] = os.path.dirname(messages.__file__)
Now you just need to call the :meth:`initialize()` function, passing in the
application's name, and you'll get an object back that you can bind to the
local ``_()`` function for run-time translations. Note that using ``_()``
isn't required, but it's a widely-used convention, derived from the `GNU
gettext `_ model.
>>> _ = initialize('flufl')
>>> print(_('A test message'))
N grfg zrffntr
It's probably best to just share this function through imports, but it does no
harm to call :meth:`initialize()` again.
>>> _ = initialize('flufl')
>>> print(_('A test message'))
N grfg zrffntr
..
>>> # Unregister the application domain used earlier. Also, clear the
>>> # environment settings from above.
>>> from flufl.i18n import registry
>>> registry._registry.clear()
>>> del os.environ['LANG']
>>> del os.environ['LOCPATH']
Multiple language contexts
==========================
Some applications, such as servers, are more complex; they need multiple
language contexts during their execution. To support this, there is a global
registry of catalog searching :doc:`strategies `. When a
particular language code is specified, a strategy is used to find the catalog
that provides that language's translations.
:doc:`flufl.i18n ` comes with a couple of fairly simple strategies,
and you can implement your own. A convenient built-in strategy looks up
catalogs from within a directory using GNU gettext conventions, where the
directory is an importable Python package (such as our ``messages`` example).
>>> from flufl.i18n import PackageStrategy
>>> strategy = PackageStrategy('flufl', messages)
The first argument is the application name, which must be unique among all
registered strategies. The second argument is the package under which the
translations can be found.
Once you have the desired strategy, register this with the global registry.
The registration process returns an application object which can be used to
look up language codes.
>>> from flufl.i18n import registry
>>> application = registry.register(strategy)
The application object keeps track of the current *translation context*,
essentially a stack of languages. This object also exports a method which you
can bind to the ``_()`` function, usually in your module globals. This
underscore function always returns translations in the language at the top of
the stack. I.e., at run time, ``_()`` will always translate its string
argument to the current context's language.
>>> _ = application._
By default the application just returns the original source string; i.e. it is
a null translator.
>>> print(_('A test message'))
A test message
And it has no language code.
>>> print(_.code)
None
You can temporarily *push* a new language context to the top of the stack, so
that the same underscore function will now return translations in the new
language context.
>>> _.push('xx')
>>> print(_.code)
xx
>>> print(_('A test message'))
N grfg zrffntr
Pop the current language to return to the default. Once you're at the bottom
of the stack, more pops will just give you the *default translation*.
Normally, that's the null translation, but as you'll see below, you can change
that too.
>>> _.pop()
>>> print(_.code)
None
>>> print(_('A test message'))
A test message
>>> _.pop()
>>> print(_.code)
None
>>> print(_('A test message'))
A test message
Context manager
===============
To make all of this more convenient, the underscore method has a context
manager called :meth:`~RuntimeTranslator.using()` which temporarily sets a new
language inside a ``with`` statement.
::
>>> print(_('A test message'))
A test message
>>> with _.using('xx'):
... print(_('A test message'))
N grfg zrffntr
>>> print(_('A test message'))
A test message
These ``with`` statements are nestable.
.. note::
The ``yy`` language is another faux translation, where the source string
is reversed.
Here we can see that the outer context translates the source string
differently than the inner context.
::
>>> with _.using('xx'):
... print(_('A test message'))
... with _.using('yy'):
... print(_('A test message'))
... print(_('A test message'))
N grfg zrffntr
egassem tset A
N grfg zrffntr
>>> print(_('A test message'))
A test message
The default language context
============================
As mentioned above, the default language context, i.e. the `Yertle`_ at the
bottom of the stack is, by default, the null translation. The null
translation just returns the source string unchanged. You can set the default
language context at the bottom of the stack.
::
>>> _.default = 'xx'
>>> print(_('A test message'))
N grfg zrffntr
>>> _.pop()
>>> print(_.code)
xx
>>> print(_('A test message'))
N grfg zrffntr
>>> with _.using('yy'):
... print(_('A test message'))
egassem tset A
>>> print(_('A test message'))
N grfg zrffntr
You can reset the default back to the null context by ``del``-ing the
:data:`~RuntimeTranslator.default` attribute.
::
>>> del _.default
>>> print(_.code)
None
Substitutions and placeholders
==============================
Once you have an underscore function, using the library is very simple. You
just call ``_()`` passing in the source string you want to translate. What if
your source strings can't be static literals, because you need them to contain
substitutions calculated at run time? You need to define some placeholders in
your source string, so that the run time substitutions will be inserted there.
Further complicating matters, some languages need to change the order of
placeholders in their translations. In general, it's good practice for your
source strings to be complete sentences, because that is easier for all your
human translaters to ... translate!
To support this, you use `PEP 292`_ style ``$``-substitution strings inside
the underscore function. These strings contain ``$variables`` and when
translated, run time data is inserted into these variables. The substitutions
are taken from the locals and globals of the function where the translation is
performed, so you don't need to repeat yourself.
Here's a simple example where the variable names are ``$ordinal`` and
``$name``. See if you can figure out where the values are taken from at
run time.
>>> ordinal = 'first'
>>> def print_it(name):
... print(_('The $ordinal test message $name'))
In this example, when ``print_it()`` is called, the ``$ordinal`` placeholder
is taken from globals, while the ``$name`` placeholder is taken from the
function's locals (i.e. its parameters and other local variables).
With no language context in place, the source string is printed unchanged,
except that the substitutions are made.
>>> print_it('Anne')
The first test message Anne
When a substitution is missing, rather than raise an exception, the
``$``-variable name itself is used.
>>> del ordinal
>>> print_it('Bart')
The $ordinal test message Bart
When there is a language context in effect, the substitutions happen after
translation.
>>> ordinal = 'second'
>>> with _.using('xx'):
... print_it('Cris')
second si n grfg zrffntr Cris
Our hypothetical ``yy`` language changes the order of the substitution
variables, but of course there is no problem with that.
>>> ordinal = 'third'
>>> with _.using('yy'):
... print_it('Dave')
Dave egassem tset third eht
Locals always take precedence over globals.
::
>>> def print_it(name, ordinal):
... print(_('The $ordinal test message $name'))
>>> with _.using('yy'):
... print_it('Elle', 'fourth')
Elle egassem tset fourth eht
Deferred translations
=====================
Remember that the ``_()`` function has both a run time and a *static* purpose.
Most source string extraction tools look for functions named ``_()`` taking a
single string argument, and the pull those arguments out as the source
strings. Sometimes however, you want to mark source strings for translation,
but you need to defer the translation of some of them until later. The way to
do this is with the :meth:`~RuntimeTranslator.defer_translation()`
function.
::
>>> with _.defer_translation():
... print(_('This gets marked but not translated'))
This gets marked but not translated
Because the string is wrapped in the ``_()`` function, it will get extracted
and added to the catalog by off-line tools, but it will not get translated
until later. This is true even if there is a translation context in effect.
>>> with _.using('xx'):
... with _.defer_translation():
... print(_('A test message'))
... print(_('A test message'))
A test message
N grfg zrffntr
.. _`PEP 292`: http://www.python.org/dev/peps/pep-0292/
.. _`Yertle`: https://en.wikipedia.org/wiki/Yertle_the_Turtle_and_Other_Stories
flufl_i18n-5.1.0/src/flufl/i18n/__init__.py 0000644 0000000 0000000 00000001755 13615410400 015203 0 ustar 00 from public import public as _public
from flufl.i18n._application import Application
from flufl.i18n._expand import expand
from flufl.i18n._registry import registry
from flufl.i18n._strategy import PackageStrategy, SimpleStrategy
from flufl.i18n.types import (
RuntimeTranslator,
TranslationContextManager,
TranslationStrategy,
)
__version__ = '5.1.0'
_public(
Application=Application,
PackageStrategy=PackageStrategy,
RuntimeTranslator=RuntimeTranslator,
SimpleStrategy=SimpleStrategy,
TranslationContextManager=TranslationContextManager,
TranslationStrategy=TranslationStrategy,
expand=expand,
registry=registry,
)
@_public
def initialize(domain: str) -> RuntimeTranslator:
"""Initialize a translation context.
:param domain: The application's name.
:return: The translation function, typically bound to _()
"""
strategy = SimpleStrategy(domain)
application = registry.register(strategy)
return application._
del _public
flufl_i18n-5.1.0/src/flufl/i18n/_application.py 0000644 0000000 0000000 00000023772 13615410400 016111 0 ustar 00 from gettext import NullTranslations
from types import TracebackType
from typing import Dict, List, Literal, NamedTuple, Optional, Type
from public import public
from flufl.i18n._translator import Translator
from flufl.i18n.types import (
RuntimeTranslator,
TranslationContextManager,
TranslationStrategy,
)
class _Using(TranslationContextManager):
"""Context manager for _.using()."""
def __init__(self, application: 'Application', language_code: str):
self._application = application
self._language_code = language_code
def __enter__(self) -> TranslationContextManager:
self._application.push(self._language_code)
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> Literal[False]:
self._application.pop()
# Do not suppress exceptions.
return False
class _Defer(TranslationContextManager):
"""Context manager for _.defer_translation()."""
# 2020-07-06(warsaw): Once Python 3.7 is the minimum supported version, we
# can use `from __future__ import annotations` instead of the string type.
def __init__(self, application: 'Application'):
self._application = application
def __enter__(self) -> TranslationContextManager:
self._application.defer()
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> Literal[False]:
self._application.pop()
# Do not suppress exceptions.
return False
class _Underscore(RuntimeTranslator):
"""The implementation of the _() function.
This class is internal representation only and has an incestuous
relationship with the Application class.
"""
# 2020-07-06(warsaw): Once Python 3.7 is the minimum supported version, we
# can use `from __future__ import annotations` instead of the string type.
def __init__(self, application: 'Application'):
self._application = application
def __call__(
self, original: str, extras: Optional[Dict[str, str]] = None
) -> str:
"""Translate the string into the language of the current context.
:param original: The original string to translate.
:param extras: Extra substitution mapping, elements of which override
the locals and globals.
:return: The translated string.
"""
return self._application.current.translate(original, extras)
def using(self, language_code: str) -> TranslationContextManager:
"""Create a context manager for temporary translation.
While in this context manager, translations use the given language
code. When the with statement exits, the original language is
restored. These are nestable.
:param language_code: The language code for the translation context.
:return: The new translation context manager.
"""
return _Using(self._application, language_code)
def defer_translation(self) -> TranslationContextManager:
"""Push a NullTranslations onto the stack.
This is useful for when you want to mark strings statically for
extraction but you want to defer translation of the string until
later.
:return: The NULLTranslations context.
"""
return _Defer(self._application)
def push(self, language_code: str) -> None:
"""Push a new catalog onto the stack.
The translation catalog associated with the language code now becomes
the currently active translation context.
:param language_code: The language code for the translation context.
"""
self._application.push(language_code)
def pop(self) -> None:
"""Pop the current catalog off the translation stack.
No exception is raised for under-runs. In that case, pop() just
no-ops and the null translation becomes the current translation
context.
"""
self._application.pop()
@property
def default(self) -> Optional[str]:
"""Return the default language code.
:return: The default language code, or None if there is no default
language.
"""
return self._application.default
@default.setter
def default(self, language_code: str) -> None:
"""Set the default language code.
:param language_code: The language code for the default translation
context.
"""
self._application.default = language_code
@default.deleter
def default(self) -> None:
"""Reset the default language to the null translator."""
del self._application.default
@property
def code(self) -> Optional[str]:
"""The language code currently in effect, if there is one."""
code = self._application.code
return self.default if code is None else code
class _StackItem(NamedTuple):
code: str
translator: Translator
@public
class Application:
"""Manage all the catalogs for a particular application.
You can ask the application for a specific catalog based on the language
code. The Application requires a strategy for finding catalog files.
Attributes:
* dedent (default True) - controls whether translated strings are dedented
or not. This is passed through to the underlying `Translator`
instance.
* depth (default 2) - The number of stack frames to call sys._getframe()
with in the underlying `Translator` instance. Passed through to that
class's constructor.
"""
def __init__(self, strategy: TranslationStrategy):
"""Create an `Application`.
Use the `dedent` attribute on this instance to control whether
translated strings are dedented or not. This is passed straight
through to the `Translator` instance created in the _() method.
:param strategy: A callable that can find catalog files for the
application based on the language code.
"""
self._strategy = strategy
# A mapping from language codes to catalogs.
self._catalogs: Dict[str, NullTranslations] = {}
self._stack: List[_StackItem] = []
# Arguments to the Translator constructor.
self.dedent = True
self.depth = 2
# By default, the baseline translator is the null translator. Use our
# public API so that we share code.
self._default_language: Optional[str]
self._default_translator: Translator
# This sets _default_language and _default_translator.
del self.default
@property
def name(self) -> str:
"""The application name.
:return: The application name.
"""
return self._strategy.name
@property
def default(self) -> Optional[str]:
"""The default language code, if there is one.
:return: The default language code or None.
"""
return self._default_language
@default.setter
def default(self, language_code: str) -> None:
"""Set the default language code.
:param language_code: The language code for the default translator.
"""
self._default_language = language_code
catalog = self.get(language_code)
self._default_translator = Translator(catalog, self.dedent, self.depth)
@default.deleter
def default(self) -> None:
"""Reset the default language to the null translator."""
self._default_language = None
self._default_translator = Translator(
self._strategy(), self.dedent, self.depth
)
def get(self, language_code: str) -> NullTranslations:
"""Get the catalog associated with the language code.
:param language_code: The language code.
:return: A `gettext` catalog.
"""
try:
return self._catalogs[language_code]
except KeyError:
catalog = self._strategy(language_code)
self._catalogs[language_code] = catalog
return catalog
@property
def _(self) -> RuntimeTranslator:
"""Return a translator object, tied to the current catalog.
:return: A translator context object for the current active catalog.
"""
return _Underscore(self)
def defer(self) -> None:
"""Push a deferred (i.e. null) translation context onto the stack.
This is primarily used to support the ``_.defer_translation()``
context manager.
"""
self._stack.append(
_StackItem(
'', Translator(NullTranslations(), self.dedent, self.depth)
)
)
def push(self, language_code: str) -> None:
"""Push a new catalog onto the stack.
The translation catalog associated with the language code now becomes
the currently active translation context.
:param language_code: The language code for the translation context.
"""
catalog = self.get(language_code)
translator = Translator(catalog, self.dedent, self.depth)
self._stack.append(_StackItem(language_code, translator))
def pop(self) -> None:
"""Pop the current catalog off the translation stack.
No exception is raised for under-runs. In that case, pop() just
no-ops and the null translation becomes the current translation
context.
"""
if len(self._stack) > 0:
self._stack.pop()
@property
def current(self) -> Translator:
"""Return the current translator.
:return: The current translator.
"""
if len(self._stack) == 0:
return self._default_translator
return self._stack[-1].translator
@property
def code(self) -> Optional[str]:
"""Return the current language code.
:return: The current language code, or None if there isn't one.
"""
if len(self._stack) == 0:
return None
return self._stack[-1].code
flufl_i18n-5.1.0/src/flufl/i18n/_expand.py 0000644 0000000 0000000 00000001446 13615410400 015057 0 ustar 00 """String interpolation."""
import logging
from string import Template
from typing import Dict
from public import public
log = logging.getLogger('flufl.i18n')
@public
def expand(
template: str,
substitutions: Dict[str, str],
template_class: type = Template,
) -> str:
"""Expand string template with substitutions.
:param template: A PEP 292 $-string template.
:param substitutions: The substitutions dictionary.
:param template_class: The template class to use.
:return: The substituted string.
"""
try:
expanded: str = template_class(template).safe_substitute(substitutions)
return expanded
except (TypeError, ValueError):
# The template is really screwed up.
log.exception('broken template: %s', template)
raise
flufl_i18n-5.1.0/src/flufl/i18n/_registry.py 0000644 0000000 0000000 00000002035 13615410400 015443 0 ustar 00 from typing import Dict
from public import public
from flufl.i18n._application import Application
from flufl.i18n.types import TranslationStrategy
@public
class Registry:
"""A registry of application translation lookup strategies."""
def __init__(self) -> None:
# Map application names to Application instances.
self._registry: Dict[str, Application] = {}
def register(self, strategy: TranslationStrategy) -> Application:
"""Add an association between an application and a lookup strategy.
:param strategy: An application translation lookup strategy.
:type application: A callable object with a .name attribute
:return: An application instance which can be used to access the
language catalogs for the application.
:rtype: `Application`
"""
application = Application(strategy)
self._registry[strategy.name] = application
return application
registry: Registry # For mypy.
public(registry=Registry())
flufl_i18n-5.1.0/src/flufl/i18n/_strategy.py 0000644 0000000 0000000 00000004503 13615410400 015437 0 ustar 00 import os
import gettext
from types import ModuleType
from typing import Optional
from public import public
from flufl.i18n.types import TranslationStrategy
class _BaseStrategy(TranslationStrategy):
"""Common code for strategies."""
def __init__(self, name: str):
"""Create a catalog lookup strategy.
:param name: The application's name.
"""
self._name = name
self._messages_dir: str
def __call__(
self, language_code: Optional[str] = None
) -> gettext.NullTranslations:
"""Find the catalog for the language.
:param language_code: The language code to find. If None, then the
default gettext language code lookup scheme is used.
:return: A `gettext` catalog.
"""
# gettext.translation() requires None or a sequence.
languages = None if language_code is None else [language_code]
try:
return gettext.translation(
self.name, self._messages_dir, languages
)
except OSError:
# Fall back to untranslated source language.
return gettext.NullTranslations()
@property
def name(self) -> str:
"""The application's name."""
return self._name
@public
class PackageStrategy(_BaseStrategy):
"""A strategy that finds catalogs based on package paths."""
def __init__(self, name: str, package: ModuleType):
"""Create a catalog lookup strategy.
:param name: The application's name.
:param package: The package path to the message catalogs. This
strategy uses the __file__ (which must exist and be a string)
of the package path as the directory containing `gettext` messages.
"""
super().__init__(name)
if hasattr(package, '__file__') and isinstance(package.__file__, str):
self._messages_dir = os.path.dirname(package.__file__)
else:
raise ValueError('Argument `package` must have a string `__file__')
@public
class SimpleStrategy(_BaseStrategy):
"""A simpler strategy for getting translations."""
def __init__(self, name: str):
"""Create a catalog lookup strategy.
:param name: The application's name.
"""
super().__init__(name)
self._messages_dir = os.environ.get('LOCPATH', '')
flufl_i18n-5.1.0/src/flufl/i18n/_substitute.py 0000644 0000000 0000000 00000001402 13615410400 016003 0 ustar 00 """Substitutions."""
import string
from typing import Union, cast
from public import public
_missing = object()
@public
class Template(string.Template):
"""Match any attribute path."""
idpattern = r'[_a-z][_a-z0-9.]*[_a-z0-9]'
@public
# Implicit generic "Any". Use "typing.Dict" and specify generic parameters
# [type-arg]
class attrdict(dict): # type: ignore
"""Follow attribute paths."""
def __getitem__(self, key: str) -> str:
parts = key.split('.')
value: Union[str, object] = super().__getitem__(parts.pop(0))
while parts:
value = getattr(value, parts.pop(0), _missing)
if value is _missing:
raise KeyError(key)
return cast(str, value)
flufl_i18n-5.1.0/src/flufl/i18n/_translator.py 0000644 0000000 0000000 00000005637 13615410400 015777 0 ustar 00 import sys
import textwrap
from gettext import NullTranslations
from typing import Dict, Optional
from public import public
from flufl.i18n._expand import expand
from flufl.i18n._substitute import Template, attrdict
@public
class Translator:
"""A translation context."""
def __init__(
self, catalog: NullTranslations, dedent: bool = True, depth: int = 2
):
"""Create a translation context.
:param catalog: The translation catalog.
:param dedent: Whether the input string should be dedented.
:param depth: Number of stack frames to call sys._getframe() with.
"""
self._catalog = catalog
self.dedent = dedent
self.depth = depth
def translate(
self, original: str, extras: Optional[Dict[str, str]] = None
) -> str:
"""Translate the string.
:param original: The original string to translate.
:param extras: Extra substitution mapping, elements of which override
the locals and globals.
:return: The translated string.
"""
if original == '':
return ''
assert original, f'Cannot translate: {original}'
# Because the original string is what the text extractors put into the
# catalog, we must first look up the original unadulterated string in
# the catalog. Use the global translation context for this.
#
# Translations must be unicode safe internally. The translation
# service is one boundary to the outside world, so to honor this
# constraint, make sure that all strings to come out of this are
# unicodes, even if the translated string or dictionary values are
# 8-bit strings.
tns = self._catalog.gettext(original)
# Do PEP 292 style $-string interpolation into the resulting string.
#
# This lets you write something like:
#
# now = time.ctime(time.time())
# print _('The current time is: $now')
#
# and have it Just Work. Key precedence is:
#
# extras > locals > globals
#
# Get the frame of the caller.
frame = sys._getframe(self.depth)
# Create the raw dictionary of substitutions.
raw_dict = frame.f_globals.copy()
raw_dict.update(frame.f_locals)
if extras is not None:
raw_dict.update(extras)
# Python 2 requires ** dictionaries to have str, not unicode keys.
# For our purposes, keys should always be ascii. Values though should
# be unicode.
translated_string: str = expand(tns, attrdict(raw_dict), Template)
# Dedent the string if so desired.
if self.dedent:
translated_string = textwrap.dedent(translated_string)
return translated_string
@property
def catalog(self) -> NullTranslations:
"""The translation catalog."""
return self._catalog
flufl_i18n-5.1.0/src/flufl/i18n/py.typed 0000644 0000000 0000000 00000000000 13615410400 014547 0 ustar 00 flufl_i18n-5.1.0/src/flufl/i18n/types.py 0000644 0000000 0000000 00000010055 13615410400 014601 0 ustar 00 from abc import ABC, abstractmethod
from gettext import NullTranslations
from types import TracebackType
from typing import Dict, Literal, Optional, Type
from public import public
@public
class TranslationContextManager(ABC):
"""Context manager for translations in a particular language."""
@abstractmethod
def __enter__(self) -> 'TranslationContextManager':
pass # pragma: nocover
@abstractmethod
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> Literal[False]:
pass # pragma nocover
@public
class RuntimeTranslator(ABC):
"""Abstract class representing the interface for the _() function."""
@abstractmethod
def __call__(
self, original: str, extras: Optional[Dict[str, str]] = None
) -> str:
"""Translate the string into the language of the current context.
This is the primary method for the runtime behavior or translating a
source string.
:param original: The original string to translate.
:param extras: Extra substitution mapping, elements of which override
the locals and globals.
:return: The translated string.
"""
@abstractmethod
def using(self, language_code: str) -> TranslationContextManager:
"""Create a context manager for temporary translation.
While in this context manager, translations use the given language
code. When the with statement exits, the original language is
restored. These are nestable.
:param language_code: The language code for the translation context.
:return: The new translation context.
"""
@abstractmethod
def defer_translation(self) -> TranslationContextManager:
"""Push a NullTranslations onto the stack.
This is useful for when you want to mark strings statically for
extraction but you want to defer translation of the string until
later.
:return: The NULLTranslations context.
"""
@abstractmethod
def push(self, language_code: str) -> None:
"""Push a new catalog onto the stack.
The translation catalog associated with the language code now becomes
the currently active translation context.
:param language_code: The language code for the translation context.
"""
@abstractmethod
def pop(self) -> None:
"""Pop the current catalog off the translation stack.
No exception is raised for under-runs. In that case, pop() just
no-ops and the null translation becomes the current translation
context.
"""
@property
@abstractmethod
def default(self) -> Optional[str]:
"""Return the default language code.
:return: The default language code, or None if there is no default
language.
"""
@default.setter
@abstractmethod
def default(self, language_code: str) -> None:
"""Set the default language code.
:param language_code: The language code for the default translation
context.
"""
@default.deleter
@abstractmethod
def default(self) -> None:
"""Reset the default language to the null translator."""
@property
@abstractmethod
def code(self) -> Optional[str]:
"""The language code currently in effect, if there is one."""
@public
class TranslationStrategy(ABC):
"""Abstract class representing the interface for translation strategies."""
@abstractmethod
def __call__(
self, language_code: Optional[str] = None
) -> NullTranslations:
"""Find the catalog for the language.
:param language_code: The language code to find. If None, then the
default gettext language code lookup scheme is used.
:return: A `gettext` catalog.
"""
@property
@abstractmethod
def name(self) -> str:
"""The application's name."""
flufl_i18n-5.1.0/test/__init__.py 0000644 0000000 0000000 00000000000 13615410400 013462 0 ustar 00 flufl_i18n-5.1.0/test/test_application.py 0000644 0000000 0000000 00000000562 13615410400 015302 0 ustar 00 import pytest
from .data import messages
from flufl.i18n import Application, PackageStrategy
def test_application_name():
strategy = PackageStrategy('flufl', messages)
application = Application(strategy)
assert application.name == 'flufl'
def test_bogus_strategy():
with pytest.raises(ValueError):
PackageStrategy('flufl', 'not-a-package')
flufl_i18n-5.1.0/test/test_expand.py 0000644 0000000 0000000 00000001515 13615410400 014255 0 ustar 00 from types import SimpleNamespace
from unittest.mock import patch
import pytest
from flufl.i18n._expand import expand
from flufl.i18n._substitute import Template
class FailingTemplateClass:
def __init__(self, template):
self.template = template
def safe_substitute(self, *args, **kws):
raise TypeError
def test_exception():
with patch('flufl.i18n._expand.log.exception') as log:
with pytest.raises(TypeError):
expand('my-template', {}, FailingTemplateClass)
log.assert_called_once_with('broken template: %s', 'my-template')
def test_trailing_dot_is_not_a_placeholder():
# GL#12 - $foo.bar.baz. interpreted the trailing dot as part of the
# placeholder, but it shouldn't be.
expanded = expand('Me and $you.', dict(you='You'), Template)
assert expanded == 'Me and You.'
flufl_i18n-5.1.0/test/test_substitute.py 0000644 0000000 0000000 00000001010 13615410400 015177 0 ustar 00 from types import SimpleNamespace
import pytest
from flufl.i18n._substitute import attrdict
def test_attrdict_parts():
ant = dict(bee=SimpleNamespace(cat=SimpleNamespace(dog='elk')))
anteater = attrdict(ant)
assert anteater['bee.cat.dog'] == 'elk'
def test_attrdict_missing():
ant = dict(bee=SimpleNamespace(cat=SimpleNamespace(dog='elk')))
anteater = attrdict(ant)
with pytest.raises(KeyError) as exc_info:
anteater['bee.cat.doo']
assert str(exc_info.value) == "'bee.cat.doo'"
flufl_i18n-5.1.0/test/test_translator.py 0000644 0000000 0000000 00000007041 13615410400 015167 0 ustar 00 # This cannot be a doctest because of the sys._getframe() manipulations. That
# does not play well with the way doctest executes Python code. But see
# translator.txt for a description of how this should work in real Python code.
import pytest
from flufl.i18n._translator import Translator
# Some globals for following tests.
purple = 'porpoises'
magenta = 'monkeys'
green = 'gerbil'
class Catalog:
"""Test catalog."""
def __init__(self):
self.translation = None
def gettext(self, original):
"""Return the translation."""
return self.translation
def charset(self):
"""Return the encoding."""
# The default is ASCII.
return None
@pytest.fixture
def translator():
# We need depth=1 because we're calling the translation at the same level
# as the locals we care about.
return Translator(Catalog(), depth=1)
def test_locals(translator):
# Test that locals get properly substituted.
aqua = 'aardvarks' # noqa: F841
blue = 'badgers' # noqa: F841
cyan = 'cats' # noqa: F841
translator.catalog.translation = '$blue and $cyan and $aqua'
translated = translator.translate('source string')
assert translated == 'badgers and cats and aardvarks'
def test_globals(translator):
# Test that globals get properly substituted.
translator.catalog.translation = '$purple and $magenta and $green'
translated = translator.translate('source string')
assert translated == 'porpoises and monkeys and gerbil'
def test_dict_overrides_locals(translator):
# Test that explicit mappings override locals.
aqua = 'aardvarks' # noqa: F841
blue = 'badgers' # noqa: F841
cyan = 'cats' # noqa: F841
overrides = dict(blue='bats')
translator.catalog.translation = '$blue and $cyan and $aqua'
translated = translator.translate('source string', overrides)
assert translated == 'bats and cats and aardvarks'
def test_globals_with_overrides(translator):
# Test that globals with overrides get properly substituted.
translator.catalog.translation = '$purple and $magenta and $green'
overrides = dict(green='giraffe')
translated = translator.translate('source string', overrides)
assert translated == 'porpoises and monkeys and giraffe'
def test_empty_string(translator):
# The empty string is always translated as the empty string.
assert translator.translate('') == ''
def test_dedent(translator):
# By default, the translated string is always dedented.
aqua = 'aardvarks' # noqa: F841
blue = 'badgers' # noqa: F841
cyan = 'cats' # noqa: F841
translator.catalog.translation = """\
These are the $blue
These are the $cyan
These are the $aqua
"""
for line in translator.translate('source string').splitlines():
assert line[:5] == 'These'
def test_no_dedent(translator):
# You can optionally suppress the dedent.
aqua = 'aardvarks' # noqa: F841
blue = 'badgers' # noqa: F841
cyan = 'cats' # noqa: F841
translator.catalog.translation = """\
These are the $blue
These are the $cyan
These are the $aqua\
"""
translator.dedent = False
for line in translator.translate('source string').splitlines():
assert line[:9] == ' These'
flufl_i18n-5.1.0/test/data/__init__.py 0000644 0000000 0000000 00000000000 13615410400 014373 0 ustar 00 flufl_i18n-5.1.0/test/data/messages/__init__.py 0000644 0000000 0000000 00000000000 13615410400 016202 0 ustar 00 flufl_i18n-5.1.0/test/data/messages/xx/LC_MESSAGES/flufl.mo 0000644 0000000 0000000 00000000772 13615410400 020002 0 ustar 00 4 L ` a p 9 A test message The $ordinal test message $name Project-Id-Version: PACKAGE VERSION
POT-Creation-Date: Sun Jan 8 15:53:47 2006
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=iso-8859-1
Content-Transfer-Encoding: 8bit
Generated-By: hand
N grfg zrffntr $ordinal si n grfg zrffntr $name flufl_i18n-5.1.0/test/data/messages/xx/LC_MESSAGES/flufl.po 0000644 0000000 0000000 00000001241 13615410400 017775 0 ustar 00 # A testing catalog
# Copyright (C) 2009-2015 Barry Warsaw
# Barry Warsaw , 2009, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: Sun Jan 8 15:53:47 2006\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=iso-8859-1\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: hand\n"
#: flufl/i18n/docs/readme.txt:39
msgid "A test message"
msgstr "N grfg zrffntr"
#: flufl/i18n/docs/readme.txt:40
msgid "The $ordinal test message $name"
msgstr "$ordinal si n grfg zrffntr $name"
flufl_i18n-5.1.0/test/data/messages/yy/LC_MESSAGES/flufl.mo 0000644 0000000 0000000 00000000771 13615410400 020003 0 ustar 00 4 L ` a p 9 A test message The $ordinal test message $name Project-Id-Version: PACKAGE VERSION
POT-Creation-Date: Sun Jan 8 15:53:47 2006
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=iso-8859-1
Content-Transfer-Encoding: 8bit
Generated-By: hand
egassem tset A $name egassem tset $ordinal eht flufl_i18n-5.1.0/test/data/messages/yy/LC_MESSAGES/flufl.po 0000644 0000000 0000000 00000001240 13615410400 017776 0 ustar 00 # A testing catalog
# Copyright (C) 2009-2015 Barry Warsaw
# Barry Warsaw , 2009, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: Sun Jan 8 15:53:47 2006\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=iso-8859-1\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: hand\n"
#: flufl/i18n/docs/readme.txt:39
msgid "A test message"
msgstr "egassem tset A"
#: flufl/i18n/docs/readme.txt:40
msgid "The $ordinal test message $name"
msgstr "$name egassem tset $ordinal eht"
flufl_i18n-5.1.0/.gitignore 0000644 0000000 0000000 00000000221 13615410400 012367 0 ustar 00 build
flufl.*.egg-info
dist
*.pyc
.tox
.coverage
coverage.xml
diffcov.html
htmlcov
/.pdm.toml
/__pypackages__/
/.DS_Store
/.pdm-python
/pdm.toml
flufl_i18n-5.1.0/LICENSE 0000644 0000000 0000000 00000001056 13615410400 011413 0 ustar 00 Copyright 2004-2024 Barry Warsaw
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
flufl_i18n-5.1.0/README.rst 0000644 0000000 0000000 00000001615 13615410400 012076 0 ustar 00 ==========
flufl.i18n
==========
A high level API for internationalizing Python libraries and applications.
The ``flufl.i18n`` library provides a convenient API for managing translation
contexts in Python applications. It provides facilities not only for
single-context applications like command line scripts, but also more
sophisticated management of multiple-context applications such as Internet
servers.
Author
======
``flufl.i18n`` is Copyright (C) 2004-2024 Barry Warsaw
Licensed under the terms of the Apache License Version 2.0. See the LICENSE
file for details.
Project details
===============
* Project home: https://gitlab.com/warsaw/flufl.i18n
* Report bugs at: https://gitlab.com/warsaw/flufl.i18n/issues
* Code hosting: https://gitlab.com/warsaw/flufl.i18n.git
* Documentation: https://flufli18n.readthedocs.io/
* PyPI: https://pypi.python.org/pypi/flufl.i18n
flufl_i18n-5.1.0/pyproject.toml 0000644 0000000 0000000 00000011700 13615410400 013317 0 ustar 00 [project]
name = 'flufl.i18n'
authors = [
{name = 'Barry Warsaw', email = 'barry@python.org'},
]
description = 'A high level API for internationalizing Python libraries and applications'
readme = 'README.rst'
requires-python = '>=3.8'
license = {text = 'Apache-2.0'}
keywords = [
'i18n',
'internationalization',
]
classifiers = [
'Development Status :: 5 - Production/Stable',
'Development Status :: 6 - Mature',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Internationalization',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Localization',
]
dependencies = [
'atpublic',
]
dynamic = ['version']
[project.urls]
'Home Page' = 'https://flufli18n.readthedocs.io'
'Documentation' = 'https://flufli18n.readthedocs.io'
'Source Code' = 'https://gitlab.com/warsaw/flufl.i18n.git'
'Bug Tracker' = 'https://gitlab.com/warsaw/flufl.i18n/issues'
[tool.hatch.version]
path = 'src/flufl/i18n/__init__.py'
[tool.hatch.build.targets.wheel]
packages = [
'src/flufl',
]
[tool.hatch.build.targets.sdist]
include = [
'src/flufl/',
'docs/',
'test/',
'conftest.py',
]
excludes = [
'**/.mypy_cache',
]
[tool.hatch.envs.default.scripts]
all = [
'hatch run test:test',
'hatch run qa:qa',
'hatch run docs:docs',
]
[[tool.hatch.envs.test.matrix]]
python = ['3.8', '3.9', '3.10', '3.11', '3.12']
[tool.hatch.envs.test]
dependencies = [
'coverage[toml]',
'diff-cover',
'pytest',
'pytest-cov',
'sybil',
]
[tool.hatch.envs.test.scripts]
test = [
'pytest {args}',
# The following is only useful in a git branch of main.
'- diff-cover coverage.xml',
]
[tool.hatch.envs.qa]
dependencies = [
'ruff',
'mypy',
]
[tool.hatch.envs.qa.env-vars]
MODULE_NAME = '{env:MODULE_NAME:flufl.i18n}'
MODULE_PATH = '{env:MODULE_PATH:src/flufl/i18n}'
[tool.hatch.envs.qa.scripts]
qa = [
'ruff check {env:MODULE_PATH}',
'mypy -p {env:MODULE_NAME}',
]
fix = [
'ruff check --fix {env:MODULE_PATH}',
]
[tool.hatch.envs.docs]
dependencies = [
'sphinx',
'furo',
]
[tool.hatch.envs.docs.scripts]
docs = [
'sphinx-build docs build/html',
]
[tool.pytest.ini_options]
addopts = '--cov=flufl --cov-report=term --cov-report=xml -p no:doctest'
testpaths = 'test docs'
[tool.coverage.paths]
source = ['flufl/i18n']
[tool.coverage.report]
fail_under = 100
show_missing = true
[tool.coverage.run]
branch = true
parallel = true
[tool.ruff]
line-length = 79
src = ['src']
[tool.ruff.lint.per-file-ignores]
# This warning is tricky because ruff wants us to remove the blank line after
# the Attributes: semi-header in the Application class docstring, but Sphinx
# wants us to add it back. Since I prefer the rendered Sphinx docs, let's
# ignore this ruff warning.
'src/flufl/i18n/_application.py' = ['D412']
[tool.ruff.lint]
select = [
'B', # flake8-bugbear
'D', # pydocstyle
'E', # pycodestyle
'F', # pyflakes
'I',
'RUF100', # check for valid noqa directives
'UP', # pyupgrade
'W', # pycodestyle
]
ignore = [
'D100', # Missing docstring in public module
'D104', # Missing docstring in public package
'D105', # Missing docstring in magic method
]
[tool.ruff.lint.pydocstyle]
convention = 'pep257'
[tool.ruff.lint.isort]
case-sensitive = true
known-first-party = ['flufl.i18n']
length-sort-straight = true
lines-after-imports = 2
lines-between-types = 1
order-by-type = true
section-order = ['standard-library', 'third-party', 'first-party']
[tool.mypy]
mypy_path = 'src'
# Disallow dynamic typing
disallow_any_generics = true
disallow_subclassing_any = true
# Untyped definitions and calls
disallow_untyped_calls = false
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = false
# None and Optional handling
no_implicit_optional = true
# Configuring warnings
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_return_any = true
warn_unreachable = true
# Miscellaneous strictness flags
implicit_reexport = false
strict_equality = true
# Configuring error messages
show_error_context = true
show_column_numbers = true
show_error_codes = true
pretty = true
show_absolute_path = true
# Miscellaneous
warn_unused_configs = true
verbosity = 0
[[tool.mypy.overrides]]
module = [
'pytest',
'sybil.*',
]
ignore_missing_imports = true
[build-system]
requires = ['hatchling']
build-backend = 'hatchling.build'
flufl_i18n-5.1.0/PKG-INFO 0000644 0000000 0000000 00000004231 13615410400 011501 0 ustar 00 Metadata-Version: 2.3
Name: flufl.i18n
Version: 5.1.0
Summary: A high level API for internationalizing Python libraries and applications
Project-URL: Home Page, https://flufli18n.readthedocs.io
Project-URL: Documentation, https://flufli18n.readthedocs.io
Project-URL: Source Code, https://gitlab.com/warsaw/flufl.i18n.git
Project-URL: Bug Tracker, https://gitlab.com/warsaw/flufl.i18n/issues
Author-email: Barry Warsaw
License: Apache-2.0
License-File: LICENSE
Keywords: i18n,internationalization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Development Status :: 6 - Mature
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Internationalization
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Localization
Requires-Python: >=3.8
Requires-Dist: atpublic
Description-Content-Type: text/x-rst
==========
flufl.i18n
==========
A high level API for internationalizing Python libraries and applications.
The ``flufl.i18n`` library provides a convenient API for managing translation
contexts in Python applications. It provides facilities not only for
single-context applications like command line scripts, but also more
sophisticated management of multiple-context applications such as Internet
servers.
Author
======
``flufl.i18n`` is Copyright (C) 2004-2024 Barry Warsaw
Licensed under the terms of the Apache License Version 2.0. See the LICENSE
file for details.
Project details
===============
* Project home: https://gitlab.com/warsaw/flufl.i18n
* Report bugs at: https://gitlab.com/warsaw/flufl.i18n/issues
* Code hosting: https://gitlab.com/warsaw/flufl.i18n.git
* Documentation: https://flufli18n.readthedocs.io/
* PyPI: https://pypi.python.org/pypi/flufl.i18n