[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install .[yaml]\n          pip install ruff pyright\n      - name: Lint with ruff\n        run: ruff check i18n\n      - name: Lint with pyright\n        run: pyright .\n      - name: Run tests\n        run: python -m i18n.tests.run_tests\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__/\n*.egg-info/\nbuild/\n*.pyc\n*~\ndist/\n.python-version\n.idea/\n.pytest_cache/\n*.lock\n.ruff_cache\n.venv"
  },
  {
    "path": ".travis.yml",
    "content": "language: python\npython:\n  - \"2.7\"\n  - \"3.6\"\n  - \"3.7\"\n  - \"3.8\"\n# command to install dependencies\ninstall: \"pip install -r requirements.txt\"\n# command to run tests\nscript:\n  - python setup.py test\n  - coverage run --source=i18n setup.py test\nafter_success:\n  - coveralls\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2012 Daniel Perez\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include *.md\ninclude LICENSE\ninclude pyproject.toml\ninclude setup.py\nrecursive-include requirements *.txt\nrecursive-include i18n *.py\nrecursive-include i18n *.json\nrecursive-include i18n *.yml\nrecursive-include i18n *.yaml\n"
  },
  {
    "path": "README.md",
    "content": "# python-i18n [![Build Status](https://github.com/danhper/python-i18n/actions/workflows/ci.yml/badge.svg)](https://github.com/danhper/python-i18n/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/danhper/python-i18n/badge.svg?branch=master)](https://coveralls.io/github/danhper/python-i18n?branch=master) [![Code Climate](https://codeclimate.com/github/danhper/python-i18n/badges/gpa.svg)](https://codeclimate.com/github/danhper/python-i18n)\n\nThis library provides i18n functionality for Python 3 out of the box. The usage is mostly based on Rails i18n library.\n\n## Installation\n\nJust run\n\n    pip install python-i18n\n\nIf you want to use YAML to store your translations, use\n\n    pip install python-i18n[YAML]\n\n## Usage\n\n### Basic usage\n\nThe simplest, though not very useful usage would be\n\n    import i18n\n    i18n.add_translation('foo', 'bar')\n    i18n.t('foo') # bar\n\n### Using translation files\n\nYAML and JSON formats are supported to store translations. With the default configuration, if you have the following `foo.en.yml` file\n\n    en:\n      hi: Hello world !\n\nin `/path/to/translations` folder, you simply need to add the folder to the translations path.\n\n    import i18n\n    i18n.load_path.append('/path/to/translations')\n    i18n.t('foo.hi') # Hello world !\n\nPlease note that YAML format is used as default file format if you have `yaml` module installed.\nIf both `yaml` and `json` modules available and you want to use JSON to store translations, explicitly specify that: `i18n.set('file_format', 'json')`\n\n### Memoization\n\nSetting the configuration value `enable_memoization` in the settings dir will load the files from disk the first time they\nare loaded and then store their content in memory. On the next use the file content will be provided from memory and not\nloaded from disk, preventing disk access. While this can be useful in some contexts, keep in mind there is no current way of\nissuing a command to the reloader to re-read the files from disk, so if you are updating your translation file without restarting\nthe interpreter do not use this option.\n\n### Namespaces\n\n#### File namespaces\n\nIn the above example, the translation key is `foo.hi` and not just `hi`. This is because the translation filename format is by default `{namespace}.{locale}.{format}`, so the {namespace} part of the file is used as translation.\n\nTo remove `{namespace}` from filename format please change the `filename_format` configuration.\n\n    i18n.set('filename_format', '{locale}.{format}')\n\n\n#### Directory namespaces\n\nIf your files are in subfolders, the foldernames are also used as namespaces, so for example if your translation root path is `/path/to/translations` and you have the file `/path/to/translations/my/app/name/foo.en.yml`, the translation namespace for the file will be `my.app.name` and the file keys will therefore be accessible from `my.app.name.foo.my_key`.\n\n## Functionalities\n\n### Placeholder\n\nYou can of course use placeholders in your translations. With the default configuration, the placeholders are used by inserting `%{placeholder_name}` in the ntranslation string. Here is a sample usage.\n\n    i18n.add_translation('hi', 'Hello %{name} !')\n    i18n.t('hi', name='Bob') # Hello Bob !\n\n### Pluralization\n\nPluralization is based on Rail i18n module. By passing a `count` variable to your translation, it will be pluralized. The translation value should be a dictionnary with at least the keys `one` and `many`. You can add a `zero` or `few` key when needed, if it is not present `many` will be used instead. Here is a sample usage.\n\n    i18n.add_translation('mail_number', {\n        'zero': 'You do not have any mail.',\n        'one': 'You have a new mail.',\n        'few': 'You only have %{count} mails.',\n        'many': 'You have %{count} new mails.'\n    })\n    i18n.t('mail_number', count=0) # You do not have any mail.\n    i18n.t('mail_number', count=1) # You have a new mail.\n    i18n.t('mail_number', count=3) # You only have 3 new mails.\n    i18n.t('mail_number', count=12) # You have 12 new mails.\n\n### Fallback\n\nYou can set a fallback which will be used when the key is not found in the default locale.\n\n    i18n.set('locale', 'jp')\n    i18n.set('fallback', 'en')\n    i18n.add_translation('foo', 'bar', locale='en')\n    i18n.t('foo') # bar\n\n### Skip locale from root\n\nSometimes i18n structure file came from another project or not contains root element with locale eg. `en` name.\n\n    {\n        \"foo\": \"FooBar\"\n    }\n\nHowever we would like to use this i18n .json file in our Python sub-project or micro service as base file for translations.\n`python-i18n` has special configuration tha is skipping locale eg. `en` root data element from the file.\n\n    i18n.set('skip_locale_root_data', True)\n"
  },
  {
    "path": "i18n/__init__.py",
    "content": "from . import config, resource_loader\nfrom .resource_loader import I18nFileLoadError as I18nFileLoadError\nfrom .resource_loader import load_config as load_config\nfrom .resource_loader import register_loader as register_loader\nfrom .translations import add as add_translation  # noqa: F401\nfrom .translator import t as t\n\nresource_loader.init_loaders()\nload_path = config.get(\"load_path\")\n"
  },
  {
    "path": "i18n/config.py",
    "content": "try:\n    __import__(\"yaml\")\n    yaml_available = True\nexcept ImportError:\n    yaml_available = False\n\ntry:\n    __import__(\"json\")\n    json_available = True\nexcept ImportError:\n    json_available = False\n\nsettings = {\n    \"filename_format\": \"{namespace}.{locale}.{format}\",\n    \"file_format\": \"yml\" if yaml_available else \"json\" if json_available else \"py\",\n    \"available_locales\": [\"en\"],\n    \"load_path\": [],\n    \"locale\": \"en\",\n    \"fallback\": \"en\",\n    \"placeholder_delimiter\": \"%\",\n    \"error_on_missing_translation\": False,\n    \"error_on_missing_placeholder\": False,\n    \"error_on_missing_plural\": False,\n    \"encoding\": \"utf-8\",\n    \"namespace_delimiter\": \".\",\n    \"plural_few\": 5,\n    \"skip_locale_root_data\": False,\n    \"enable_memoization\": False,\n}\n\n\ndef set(key, value):\n    settings[key] = value\n\n\ndef get(key):\n    return settings[key]\n"
  },
  {
    "path": "i18n/loaders/__init__.py",
    "content": ""
  },
  {
    "path": "i18n/loaders/json_loader.py",
    "content": "import json\n\nfrom .loader import I18nFileLoadError, Loader\n\n\nclass JsonLoader(Loader):\n    \"\"\"class to load yaml files\"\"\"\n\n    def __init__(self):\n        super(JsonLoader, self).__init__()\n\n    def parse_file(self, file_content):\n        try:\n            return json.loads(file_content)\n        except ValueError as e:\n            raise I18nFileLoadError(\"invalid JSON: {0}\".format(str(e)))\n"
  },
  {
    "path": "i18n/loaders/loader.py",
    "content": "import io\n\nfrom .. import config\n\n\nclass I18nFileLoadError(Exception):\n    def __init__(self, value):\n        self.value = value\n\n    def __str__(self):\n        return str(self.value)\n\n\nclass Loader(object):\n    \"\"\"Base class to load resources\"\"\"\n\n    def __init__(self):\n        super(Loader, self).__init__()\n        self.memoization_dict = {}\n\n    def _load_file_data(self, filename):\n        try:\n            with io.open(filename, \"r\", encoding=config.get(\"encoding\")) as f:\n                return f.read()\n        except IOError as e:\n            raise I18nFileLoadError(\n                \"error loading file {0}: {1}\".format(filename, e.strerror)\n            )\n\n    def load_file(self, filename):\n        enable_memoization = config.get(\"enable_memoization\")\n        if enable_memoization:\n            if filename not in self.memoization_dict:\n                self.memoization_dict[filename] = self._load_file_data(filename)\n            return self.memoization_dict[filename]\n        else:\n            return self._load_file_data(filename)\n\n    def parse_file(self, file_content):\n        raise NotImplementedError(\n            \"the method parse_file has not been implemented for class {0}\".format(\n                self.__class__.__name__\n            )\n        )\n\n    def check_data(self, data, root_data):\n        return True if root_data is None else root_data in data\n\n    def get_data(self, data, root_data):\n        return data if root_data is None else data[root_data]\n\n    def load_resource(self, filename, root_data):\n        file_content = self.load_file(filename)\n        data = self.parse_file(file_content)\n        if not self.check_data(data, root_data):\n            raise I18nFileLoadError(\n                \"error getting data from {0}: {1} not defined\".format(\n                    filename, root_data\n                )\n            )\n        return self.get_data(data, root_data)\n"
  },
  {
    "path": "i18n/loaders/python_loader.py",
    "content": "import os.path\nimport sys\n\nfrom .loader import I18nFileLoadError, Loader\n\n\nclass PythonLoader(Loader):\n    \"\"\"class to load python files\"\"\"\n\n    def __init__(self):\n        super(PythonLoader, self).__init__()\n\n    def load_file(self, filename):\n        path, name = os.path.split(filename)\n        module_name, ext = os.path.splitext(name)\n        if path not in sys.path:\n            sys.path.append(path)\n        try:\n            return __import__(module_name)\n        except ImportError:\n            raise I18nFileLoadError(\"error loading file {0}\".format(filename))\n\n    def parse_file(self, file_content):\n        return file_content\n\n    def check_data(self, data, root_data):\n        return hasattr(data, root_data)\n\n    def get_data(self, data, root_data):\n        return getattr(data, root_data)\n"
  },
  {
    "path": "i18n/loaders/yaml_loader.py",
    "content": "import yaml\n\nfrom .loader import I18nFileLoadError, Loader\n\n\nclass YamlLoader(Loader):\n    \"\"\"class to load yaml files\"\"\"\n\n    def __init__(self):\n        super(YamlLoader, self).__init__()\n\n    def parse_file(self, file_content):\n        try:\n            loader = getattr(yaml, \"FullLoader\", yaml.SafeLoader)\n            return yaml.load(file_content, Loader=loader)\n        except Exception as e:\n            raise I18nFileLoadError(\"invalid YAML: {0}\".format(str(e)))\n"
  },
  {
    "path": "i18n/resource_loader.py",
    "content": "import os.path\n\nfrom . import config, translations\nfrom .loaders.loader import I18nFileLoadError\n\nloaders = {}\n\nPLURALS = [\"zero\", \"one\", \"few\", \"many\", \"other\"]\n\n\ndef register_loader(loader_class, supported_extensions):\n    if not hasattr(loader_class, \"load_resource\"):\n        raise ValueError(\"loader class should have a 'load_resource' method\")\n\n    for extension in supported_extensions:\n        loaders[extension] = loader_class()\n\n\ndef load_resource(filename, root_data):\n    extension = os.path.splitext(filename)[1][1:]\n    if extension not in loaders:\n        raise I18nFileLoadError(\n            \"no loader available for extension {0}\".format(extension)\n        )\n    return getattr(loaders[extension], \"load_resource\")(filename, root_data)\n\n\ndef init_loaders():\n    init_python_loader()\n    if config.yaml_available:\n        init_yaml_loader()\n    if config.json_available:\n        init_json_loader()\n\n\ndef init_python_loader():\n    from .loaders.python_loader import PythonLoader\n\n    register_loader(PythonLoader, [\"py\"])\n\n\ndef init_yaml_loader():\n    from .loaders.yaml_loader import YamlLoader\n\n    register_loader(YamlLoader, [\"yml\", \"yaml\"])\n\n\ndef init_json_loader():\n    from .loaders.json_loader import JsonLoader\n\n    register_loader(JsonLoader, [\"json\"])\n\n\ndef load_config(filename):\n    settings_data = load_resource(filename, \"settings\")\n    for key, value in settings_data.items():\n        config.settings[key] = value\n\n\ndef get_namespace_from_filepath(filename):\n    namespace = (\n        os.path.dirname(filename)\n        .strip(os.sep)\n        .replace(os.sep, config.get(\"namespace_delimiter\"))\n    )\n    if \"{namespace}\" in config.get(\"filename_format\"):\n        try:\n            splitted_filename = os.path.basename(filename).split(\".\")\n            if namespace:\n                namespace += config.get(\"namespace_delimiter\")\n            namespace += splitted_filename[\n                config.get(\"filename_format\").index(\"{namespace}\")\n            ]\n        except ValueError:\n            raise I18nFileLoadError(\"incorrect file format.\")\n    return namespace\n\n\ndef load_translation_file(filename, base_directory, locale=config.get(\"locale\")):\n    skip_locale_root_data = config.get(\"skip_locale_root_data\")\n    root_data = None if skip_locale_root_data else locale\n    translations_dic = load_resource(os.path.join(base_directory, filename), root_data)\n    namespace = get_namespace_from_filepath(filename)\n    load_translation_dic(translations_dic, namespace, locale)\n\n\ndef load_translation_dic(dic, namespace, locale):\n    if namespace:\n        namespace += config.get(\"namespace_delimiter\")\n    for key, value in dic.items():\n        if isinstance(value, dict) and len(set(PLURALS).intersection(value)) < 2:\n            load_translation_dic(value, namespace + key, locale)\n        else:\n            translations.add(namespace + key, value, locale)\n\n\ndef load_directory(directory, locale=config.get(\"locale\")):\n    for f in os.listdir(directory):\n        path = os.path.join(directory, f)\n        if os.path.isfile(path) and path.endswith(config.get(\"file_format\")):\n            if \"{locale}\" in config.get(\"filename_format\") and locale not in f:\n                continue\n            load_translation_file(f, directory, locale)\n\n\ndef search_translation(key, locale=config.get(\"locale\")):\n    splitted_key = key.split(config.get(\"namespace_delimiter\"))\n    if not splitted_key:\n        return\n    namespace = splitted_key[:-1]\n    if not namespace and \"{namespace}\" not in config.get(\"filename_format\"):\n        for directory in config.get(\"load_path\"):\n            load_directory(directory, locale)\n    else:\n        for directory in config.get(\"load_path\"):\n            recursive_search_dir(namespace, \"\", directory, locale)\n\n\ndef recursive_search_dir(\n    splitted_namespace, directory, root_dir, locale=config.get(\"locale\")\n):\n    if not splitted_namespace:\n        return\n    seeked_file = config.get(\"filename_format\").format(\n        namespace=splitted_namespace[0], format=config.get(\"file_format\"), locale=locale\n    )\n    dir_content = os.listdir(os.path.join(root_dir, directory))\n    if seeked_file in dir_content:\n        load_translation_file(os.path.join(directory, seeked_file), root_dir, locale)\n    elif splitted_namespace[0] in dir_content:\n        recursive_search_dir(\n            splitted_namespace[1:],\n            os.path.join(directory, splitted_namespace[0]),\n            root_dir,\n            locale,\n        )\n"
  },
  {
    "path": "i18n/tests/__init__.py",
    "content": ""
  },
  {
    "path": "i18n/tests/loader_tests.py",
    "content": "# -*- encoding: utf-8 -*-\n\nfrom __future__ import unicode_literals\n\nimport os\nimport os.path\nimport tempfile\nimport unittest\n\n# Python 3 only: always import reload from importlib\nfrom importlib import reload\n\nfrom i18n import config, resource_loader, translations\nfrom i18n.config import json_available, yaml_available\nfrom i18n.resource_loader import I18nFileLoadError\nfrom i18n.translator import t\n\nRESOURCE_FOLDER = os.path.join(os.path.dirname(__file__), \"resources\")\n\n\nclass TestFileLoader(unittest.TestCase):\n    def setUp(self):\n        resource_loader.loaders = {}\n        translations.container = {}\n        reload(config)\n        config.set(\"load_path\", [os.path.join(RESOURCE_FOLDER, \"translations\")])\n        config.set(\"filename_format\", \"{namespace}.{locale}.{format}\")\n        config.set(\"encoding\", \"utf-8\")\n\n    def test_load_unavailable_extension(self):\n        with self.assertRaisesRegex(I18nFileLoadError, \"no loader .*\"):\n            resource_loader.load_resource(\"foo.bar\", \"baz\")\n            with self.assertRaisesRegex(I18nFileLoadError, \"no loader .*\"):\n                resource_loader.load_resource(\"foo.bar\", \"baz\")\n\n    def test_register_wrong_loader(self):\n        class WrongLoader(object):\n            pass\n\n        with self.assertRaises(ValueError):\n            resource_loader.register_loader(WrongLoader, [])\n\n    def test_register_python_loader(self):\n        resource_loader.init_python_loader()\n        with self.assertRaisesRegex(I18nFileLoadError, \"error loading file .*\"):\n            resource_loader.load_resource(\"foo.py\", \"bar\")\n            with self.assertRaisesRegex(I18nFileLoadError, \"error loading file .*\"):\n                resource_loader.load_resource(\"foo.py\", \"bar\")\n\n    @unittest.skipUnless(yaml_available, \"yaml library not available\")\n    def test_register_yaml_loader(self):\n        resource_loader.init_yaml_loader()\n        with self.assertRaisesRegex(I18nFileLoadError, \"error loading file .*\"):\n            resource_loader.load_resource(\"foo.yml\", \"bar\")\n            with self.assertRaisesRegex(I18nFileLoadError, \"error loading file .*\"):\n                resource_loader.load_resource(\"foo.yml\", \"bar\")\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_load_wrong_json_file(self):\n        resource_loader.init_json_loader()\n        with self.assertRaisesRegex(I18nFileLoadError, \"error getting data .*\"):\n            resource_loader.load_resource(\n                os.path.join(RESOURCE_FOLDER, \"settings\", \"dummy_config.json\"), \"foo\"\n            )\n            with self.assertRaisesRegex(I18nFileLoadError, \"error getting data .*\"):\n                resource_loader.load_resource(\n                    os.path.join(RESOURCE_FOLDER, \"settings\", \"dummy_config.json\"),\n                    \"foo\",\n                )\n\n    @unittest.skipUnless(yaml_available, \"yaml library not available\")\n    def test_load_yaml_file(self):\n        resource_loader.init_yaml_loader()\n        data = resource_loader.load_resource(\n            os.path.join(RESOURCE_FOLDER, \"settings\", \"dummy_config.yml\"), \"settings\"\n        )\n        self.assertIn(\"foo\", data)\n        self.assertEqual(\"bar\", data[\"foo\"])\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_load_json_file(self):\n        resource_loader.init_json_loader()\n        data = resource_loader.load_resource(\n            os.path.join(RESOURCE_FOLDER, \"settings\", \"dummy_config.json\"), \"settings\"\n        )\n        self.assertIn(\"foo\", data)\n        self.assertEqual(\"bar\", data[\"foo\"])\n\n    def test_load_python_file(self):\n        resource_loader.init_python_loader()\n        data = resource_loader.load_resource(\n            os.path.join(RESOURCE_FOLDER, \"settings\", \"dummy_config.py\"), \"settings\"\n        )\n        self.assertIn(\"foo\", data)\n        self.assertEqual(\"bar\", data[\"foo\"])\n\n    @unittest.skipUnless(yaml_available, \"yaml library not available\")\n    def test_memoization_with_file(self):\n        \"\"\"This test creates a temporary file with the help of the\n        tempfile library and writes a simple key: value dictionary in it.\n        It will then use that file to load the translations and, after having\n        enabled memoization, try to access it, causing the file to be (hopefully)\n        memoized. It will then _remove_ the temporary file and try to access again,\n        asserting that an error is not raised, thus making sure the data is\n        actually loaded from memory and not from disk access.\"\"\"\n        memoization_file_name = \"memoize.en.yml\"\n        # create the file and write the data in it\n        try:\n            d = tempfile.TemporaryDirectory()\n            tmp_dir_name = d.name\n        except AttributeError:\n            # we are running python2, use mkdtemp\n            tmp_dir_name = tempfile.mkdtemp()\n        fd = open(\"{}/{}\".format(tmp_dir_name, memoization_file_name), \"w\")\n        fd.write(\"en:\\n  key: value\")\n        fd.close()\n        # create the loader and pass the file to it\n        resource_loader.init_yaml_loader()\n        resource_loader.load_translation_file(memoization_file_name, tmp_dir_name)\n        # try loading the value to make sure it's working\n        self.assertEqual(t(\"memoize.key\"), \"value\")\n        # now delete the file and directory\n        # we are running python2, delete manually\n        import shutil\n\n        shutil.rmtree(tmp_dir_name)\n        # test the translation again to make sure it's loaded from memory\n        self.assertEqual(t(\"memoize.key\"), \"value\")\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_load_file_with_strange_encoding(self):\n        resource_loader.init_json_loader()\n        config.set(\"encoding\", \"euc-jp\")\n        data = resource_loader.load_resource(\n            os.path.join(RESOURCE_FOLDER, \"settings\", \"eucjp_config.json\"), \"settings\"\n        )\n        self.assertIn(\"ほげ\", data)\n        self.assertEqual(\"ホゲ\", data[\"ほげ\"])\n\n    def test_get_namespace_from_filepath_with_filename(self):\n        tests = {\n            \"foo\": \"foo.ja.yml\",\n            \"foo.bar\": os.path.join(\"foo\", \"bar.ja.yml\"),\n            \"foo.bar.baz\": os.path.join(\"foo\", \"bar\", \"baz.en.yml\"),\n        }\n        for expected, test_val in tests.items():\n            namespace = resource_loader.get_namespace_from_filepath(test_val)\n            self.assertEqual(expected, namespace)\n\n    def test_get_namespace_from_filepath_without_filename(self):\n        tests = {\n            \"\": \"ja.yml\",\n            \"foo\": os.path.join(\"foo\", \"ja.yml\"),\n            \"foo.bar\": os.path.join(\"foo\", \"bar\", \"en.yml\"),\n        }\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        for expected, test_val in tests.items():\n            namespace = resource_loader.get_namespace_from_filepath(test_val)\n            self.assertEqual(expected, namespace)\n\n    @unittest.skipUnless(yaml_available, \"yaml library not available\")\n    def test_load_translation_file(self):\n        resource_loader.init_yaml_loader()\n        resource_loader.load_translation_file(\n            \"foo.en.yml\", os.path.join(RESOURCE_FOLDER, \"translations\")\n        )\n\n        self.assertTrue(translations.has(\"foo.normal_key\"))\n        self.assertTrue(translations.has(\"foo.parent.nested_key\"))\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_load_plural(self):\n        resource_loader.init_yaml_loader()\n        resource_loader.load_translation_file(\n            \"foo.en.yml\", os.path.join(RESOURCE_FOLDER, \"translations\")\n        )\n        self.assertTrue(translations.has(\"foo.mail_number\"))\n        translated_plural = translations.get(\"foo.mail_number\")\n        self.assertIsInstance(translated_plural, dict)\n        self.assertEqual(translated_plural[\"zero\"], \"You do not have any mail.\")\n        self.assertEqual(translated_plural[\"one\"], \"You have a new mail.\")\n        self.assertEqual(translated_plural[\"many\"], \"You have %{count} new mails.\")\n\n    @unittest.skipUnless(yaml_available, \"yaml library not available\")\n    def test_search_translation_yaml(self):\n        resource_loader.init_yaml_loader()\n        config.set(\"file_format\", \"yml\")\n        resource_loader.search_translation(\"foo.normal_key\")\n        self.assertTrue(translations.has(\"foo.normal_key\"))\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_search_translation_json(self):\n        resource_loader.init_json_loader()\n        config.set(\"file_format\", \"json\")\n\n        resource_loader.search_translation(\"bar.baz.qux\")\n        self.assertTrue(translations.has(\"bar.baz.qux\"))\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_search_translation_without_ns(self):\n        resource_loader.init_json_loader()\n        config.set(\"file_format\", \"json\")\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        resource_loader.search_translation(\"foo\")\n        self.assertTrue(translations.has(\"foo\"))\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_search_translation_without_ns_nested_dict__two_levels_neting__default_locale(\n        self,\n    ):\n        resource_loader.init_json_loader()\n        config.set(\"file_format\", \"json\")\n        config.set(\n            \"load_path\",\n            [os.path.join(RESOURCE_FOLDER, \"translations\", \"nested_dict_json\")],\n        )\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"skip_locale_root_data\", True)\n        config.set(\"locale\", [\"en\", \"pl\"])\n        resource_loader.search_translation(\"COMMON.VERSION\")\n        self.assertTrue(translations.has(\"COMMON.VERSION\"))\n        self.assertEqual(translations.get(\"COMMON.VERSION\"), \"version\")\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_search_translation_without_ns_nested_dict__two_levels_neting__other_locale(\n        self,\n    ):\n        resource_loader.init_json_loader()\n        config.set(\"file_format\", \"json\")\n        config.set(\n            \"load_path\",\n            [os.path.join(RESOURCE_FOLDER, \"translations\", \"nested_dict_json\")],\n        )\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"skip_locale_root_data\", True)\n        config.set(\"locale\", [\"en\", \"pl\"])\n        resource_loader.search_translation(\"COMMON.VERSION\", locale=\"pl\")\n        self.assertTrue(translations.has(\"COMMON.VERSION\", locale=\"pl\"))\n        self.assertEqual(translations.get(\"COMMON.VERSION\", locale=\"pl\"), \"wersja\")\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_search_translation_without_ns_nested_dict__default_locale(self):\n        resource_loader.init_json_loader()\n        config.set(\"file_format\", \"json\")\n        config.set(\n            \"load_path\",\n            [os.path.join(RESOURCE_FOLDER, \"translations\", \"nested_dict_json\")],\n        )\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"skip_locale_root_data\", True)\n        config.set(\"locale\", \"en\")\n        resource_loader.search_translation(\"TOP_MENU.TOP_BAR.LOGS\")\n        self.assertTrue(translations.has(\"TOP_MENU.TOP_BAR.LOGS\"))\n        self.assertEqual(translations.get(\"TOP_MENU.TOP_BAR.LOGS\"), \"Logs\")\n\n    @unittest.skipUnless(json_available, \"json library not available\")\n    def test_search_translation_without_ns_nested_dict__other_locale(self):\n        resource_loader.init_json_loader()\n        config.set(\"file_format\", \"json\")\n        config.set(\n            \"load_path\",\n            [os.path.join(RESOURCE_FOLDER, \"translations\", \"nested_dict_json\")],\n        )\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"skip_locale_root_data\", True)\n        config.set(\"locale\", \"en\")\n        resource_loader.search_translation(\"TOP_MENU.TOP_BAR.LOGS\", locale=\"pl\")\n        self.assertTrue(translations.has(\"TOP_MENU.TOP_BAR.LOGS\", locale=\"pl\"))\n        self.assertEqual(translations.get(\"TOP_MENU.TOP_BAR.LOGS\", locale=\"pl\"), \"Logi\")\n\n\nsuite = unittest.TestLoader().loadTestsFromTestCase(TestFileLoader)\nunittest.TextTestRunner(verbosity=2).run(suite)\n"
  },
  {
    "path": "i18n/tests/resources/settings/dummy_config.json",
    "content": "{\n    \"settings\" : {\n        \"foo\": \"bar\",\n        \"baz\": \"qux\"\n    }\n}\n"
  },
  {
    "path": "i18n/tests/resources/settings/dummy_config.py",
    "content": "settings = {\"foo\": \"bar\", \"baz\": \"qux\"}\n"
  },
  {
    "path": "i18n/tests/resources/settings/dummy_config.yml",
    "content": "settings:\n  foo: bar\n  baz: qux\n"
  },
  {
    "path": "i18n/tests/resources/settings/eucjp_config.json",
    "content": "{\n    \"settings\": {\n        \"ۤ\": \"ۥ\"\n    }\n}\n"
  },
  {
    "path": "i18n/tests/resources/translations/bar/baz.en.json",
    "content": "{\n    \"en\": {\n        \"qux\": \"hoge\"\n    }\n}\n"
  },
  {
    "path": "i18n/tests/resources/translations/en.json",
    "content": "{\n  \"en\": {\n    \"foo\": \"FooBar\"\n  }\n}\n"
  },
  {
    "path": "i18n/tests/resources/translations/foo.en.yml",
    "content": "en:\n  normal_key: normal_value\n  parent:\n    nested_key: nested_value\n  mail_number:\n    zero: You do not have any mail.\n    one: You have a new mail.\n    few: You only have %{count} mails.\n    many: You have %{count} new mails.\n"
  },
  {
    "path": "i18n/tests/resources/translations/foo.ja.yml",
    "content": "ja:\n  normal_key: 普通\n  fallback_key: フォールバック\n"
  },
  {
    "path": "i18n/tests/resources/translations/gb.json",
    "content": "{\n  \"foo\": \"Lorry\"\n}"
  },
  {
    "path": "i18n/tests/resources/translations/ja.json",
    "content": "{\n  \"ja\": {\n    \"foo\": \"ふぉお\",\n    \"bar\": \"ばば\"\n  }\n}\n"
  },
  {
    "path": "i18n/tests/resources/translations/nested_dict_json/en.json",
    "content": "{\n  \"TOP_MENU\": {\n    \"TOP_BAR\": {\n      \"LOGS\": \"Logs\",\n      \"LOGS_ITEMS\": {\n        \"MAIN\": \"Main process\",\n        \"ERROR\": \"Errors\",\n        \"CUSTOM\": \"Custom Logs\"\n      },\n      \"NOTIFICATIONS\": \"Notifications\",\n      \"CONFIG\": \"Config\",\n      \"LICENSE\": \"License\"\n    }\n  },\n  \"COMMON\": {\n    \"HOME\": \"Home\",\n    \"START\": \"Start\",\n    \"STOP\": \"Stop\",\n    \"REFRESH\": \"Refresh\",\n    \"LOAD_FILE\": \"Load file\",\n    \"DELETE_FILE\": \"Delete file\",\n    \"EXECUTE\": \"Execute\",\n    \"DELETE\": \"Delete\",\n    \"VERSION\": \"version\",\n    \"SUBMIT\": \"Submit\"\n  }\n}"
  },
  {
    "path": "i18n/tests/resources/translations/nested_dict_json/pl.json",
    "content": "{\n  \"TOP_MENU\": {\n    \"TOP_BAR\": {\n       \"LOGS\": \"Logi\",\n         \"LOGS_ITEMS\": {\n           \"MAIN\": \"Główny proces\",\n           \"ERROR\": \"Błędy\",\n           \"CUSTOM\": \"Logi dodatkowe\"\n            },\n       \"NOTIFICATIONS\": \"Powiadomienia\",\n       \"CONFIG\": \"Konfiguracja\",\n       \"LICENSE\": \"Licencja\"\n    }\n  },\n   \"COMMON\": {\n    \"HOME\": \"Początek\",\n    \"START\": \"Start\",\n    \"STOP\": \"Stop\",\n    \"REFRESH\": \"Odśwież\",\n    \"LOAD_FILE\": \"Załaduj\",\n    \"DELETE_FILE\": \"Usuń\",\n    \"EXECUTE\": \"Wykonaj\",\n    \"DELETE\": \"Usuń\",\n    \"VERSION\": \"wersja\",\n    \"SUBMIT\": \"Wyślij\"\n  }\n}\n"
  },
  {
    "path": "i18n/tests/run_tests.py",
    "content": "import unittest\n\nfrom i18n.tests.loader_tests import TestFileLoader\nfrom i18n.tests.translation_tests import TestTranslationFormat\n\n\ndef suite():\n    suite = unittest.TestSuite()\n    loader = unittest.TestLoader()\n    suite.addTest(loader.loadTestsFromTestCase(TestFileLoader))\n    suite.addTest(loader.loadTestsFromTestCase(TestTranslationFormat))\n    return suite\n\n\nif __name__ == \"__main__\":\n    runner = unittest.TextTestRunner()\n    test_suite = suite()\n    runner.run(test_suite)\n"
  },
  {
    "path": "i18n/tests/translation_tests.py",
    "content": "# -*- encoding: utf-8 -*-\n\nfrom __future__ import unicode_literals\n\nimport os\nimport os.path\nimport unittest\n\n# Python 3 only: always import reload from importlib\nfrom importlib import reload\n\nfrom i18n import config, resource_loader, translations\nfrom i18n.translator import t\n\nRESOURCE_FOLDER = os.path.dirname(__file__) + os.sep + \"resources\" + os.sep\n\n\nclass TestTranslationFormat(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        resource_loader.init_loaders()\n        reload(config)\n        config.set(\"load_path\", [os.path.join(RESOURCE_FOLDER, \"translations\")])\n        translations.add(\"foo.hi\", \"Hello %{name} !\")\n        translations.add(\"foo.hello\", \"Salut %{name} !\", locale=\"fr\")\n        translations.add(\n            \"foo.basic_plural\", {\"one\": \"1 elem\", \"many\": \"%{count} elems\"}\n        )\n        translations.add(\n            \"foo.plural\",\n            {\n                \"zero\": \"no mail\",\n                \"one\": \"1 mail\",\n                \"few\": \"only %{count} mails\",\n                \"many\": \"%{count} mails\",\n            },\n        )\n        translations.add(\"foo.bad_plural\", {\"bar\": \"foo elems\"})\n\n    def setUp(self):\n        config.set(\"error_on_missing_translation\", False)\n        config.set(\"error_on_missing_placeholder\", False)\n        config.set(\"fallback\", \"en\")\n        config.set(\"locale\", \"en\")\n\n    def test_basic_translation(self):\n        self.assertEqual(t(\"foo.normal_key\"), \"normal_value\")\n\n    def test_missing_translation(self):\n        self.assertEqual(t(\"foo.inexistent\"), \"foo.inexistent\")\n\n    def test_missing_translation_error(self):\n        config.set(\"error_on_missing_translation\", True)\n        with self.assertRaises(KeyError):\n            t(\"foo.inexistent\")\n\n    def test_locale_change(self):\n        config.set(\"locale\", \"fr\")\n        self.assertEqual(t(\"foo.hello\", name=\"Bob\"), \"Salut Bob !\")\n\n    def test_fallback(self):\n        config.set(\"fallback\", \"fr\")\n        self.assertEqual(t(\"foo.hello\", name=\"Bob\"), \"Salut Bob !\")\n\n    def test_fallback_from_resource(self):\n        config.set(\"fallback\", \"ja\")\n        self.assertEqual(t(\"foo.fallback_key\"), \"フォールバック\")\n\n    def test_basic_placeholder(self):\n        self.assertEqual(t(\"foo.hi\", name=\"Bob\"), \"Hello Bob !\")\n\n    def test_missing_placehoder(self):\n        self.assertEqual(t(\"foo.hi\"), \"Hello %{name} !\")\n\n    def test_missing_placeholder_error(self):\n        config.set(\"error_on_missing_placeholder\", True)\n        with self.assertRaises(KeyError):\n            t(\"foo.hi\")\n\n    def test_basic_pluralization(self):\n        self.assertEqual(t(\"foo.basic_plural\", count=0), \"0 elems\")\n        self.assertEqual(t(\"foo.basic_plural\", count=1), \"1 elem\")\n        self.assertEqual(t(\"foo.basic_plural\", count=2), \"2 elems\")\n\n    def test_full_pluralization(self):\n        self.assertEqual(t(\"foo.plural\", count=0), \"no mail\")\n        self.assertEqual(t(\"foo.plural\", count=1), \"1 mail\")\n        self.assertEqual(t(\"foo.plural\", count=4), \"only 4 mails\")\n        self.assertEqual(t(\"foo.plural\", count=12), \"12 mails\")\n\n    def test_bad_pluralization(self):\n        config.set(\"error_on_missing_plural\", False)\n        self.assertEqual(t(\"foo.normal_key\", count=5), \"normal_value\")\n        config.set(\"error_on_missing_plural\", True)\n        with self.assertRaises(KeyError):\n            t(\"foo.bad_plural\", count=0)\n\n    def test_default(self):\n        self.assertEqual(t(\"inexistent_key\", default=\"foo\"), \"foo\")\n\n    def test_skip_locale_root_data(self):\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"file_format\", \"json\")\n        config.set(\"locale\", \"gb\")\n        config.set(\"skip_locale_root_data\", True)\n        resource_loader.init_loaders()\n        self.assertEqual(t(\"foo\"), \"Lorry\")\n        config.set(\"skip_locale_root_data\", False)\n\n    def test_skip_locale_root_data_nested_json_dict__default_locale(self):\n        config.set(\"file_format\", \"json\")\n        config.set(\n            \"load_path\",\n            [os.path.join(RESOURCE_FOLDER, \"translations\", \"nested_dict_json\")],\n        )\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"skip_locale_root_data\", True)\n        config.set(\"locale\", \"en\")\n        resource_loader.init_json_loader()\n        self.assertEqual(t(\"COMMON.START\"), \"Start\")\n\n    def test_skip_locale_root_data_nested_json_dict__other_locale(self):\n        config.set(\"file_format\", \"json\")\n        config.set(\n            \"load_path\",\n            [os.path.join(RESOURCE_FOLDER, \"translations\", \"nested_dict_json\")],\n        )\n        config.set(\"filename_format\", \"{locale}.{format}\")\n        config.set(\"skip_locale_root_data\", True)\n        config.set(\"locale\", \"en\")\n        resource_loader.init_json_loader()\n        self.assertEqual(t(\"COMMON.EXECUTE\", locale=\"pl\"), \"Wykonaj\")\n"
  },
  {
    "path": "i18n/translations.py",
    "content": "from . import config\n\ncontainer = {}\n\n\ndef add(key, value, locale=config.get(\"locale\")):\n    container.setdefault(locale, {})[key] = value\n\n\ndef has(key, locale=config.get(\"locale\")):\n    return key in container.get(locale, {})\n\n\ndef get(key, locale=config.get(\"locale\")):\n    return container[locale][key]\n"
  },
  {
    "path": "i18n/translator.py",
    "content": "from string import Template\n\nfrom . import config, resource_loader, translations\n\n\nclass TranslationFormatter(Template):\n    delimiter = config.get(\"placeholder_delimiter\")\n\n    def __init__(self, template):\n        super(TranslationFormatter, self).__init__(template)\n\n    def format(self, **kwargs):\n        if config.get(\"error_on_missing_placeholder\"):\n            return self.substitute(**kwargs)\n        else:\n            return self.safe_substitute(**kwargs)\n\n\ndef t(key, **kwargs):\n    locale = kwargs.pop(\"locale\", config.get(\"locale\"))\n    if translations.has(key, locale):\n        return translate(key, locale=locale, **kwargs)\n    else:\n        resource_loader.search_translation(key, locale)\n        if translations.has(key, locale):\n            return translate(key, locale=locale, **kwargs)\n        elif locale != config.get(\"fallback\"):\n            return t(key, locale=config.get(\"fallback\"), **kwargs)\n    if \"default\" in kwargs:\n        return kwargs[\"default\"]\n    if config.get(\"error_on_missing_translation\"):\n        raise KeyError(\"key {0} not found\".format(key))\n    else:\n        return key\n\n\ndef translate(key, **kwargs):\n    locale = kwargs.pop(\"locale\", config.get(\"locale\"))\n    translation = translations.get(key, locale=locale)\n    if \"count\" in kwargs:\n        translation = pluralize(key, translation, kwargs[\"count\"])\n    return TranslationFormatter(translation).format(**kwargs)\n\n\ndef pluralize(key, translation, count):\n    return_value = key\n    try:\n        if not isinstance(translation, dict):\n            return_value = translation\n            raise KeyError(\"use of count witouth dict for key {0}\".format(key))\n        if count == 0:\n            if \"zero\" in translation:\n                return translation[\"zero\"]\n        elif count == 1:\n            if \"one\" in translation:\n                return translation[\"one\"]\n        elif count <= config.get(\"plural_few\"):\n            if \"few\" in translation:\n                return translation[\"few\"]\n        # TODO: deprecate other\n        if \"other\" in translation:\n            return translation[\"other\"]\n        if \"many\" in translation:\n            return translation[\"many\"]\n        else:\n            raise KeyError('\"many\" not defined for key {0}'.format(key))\n    except KeyError as e:\n        if config.get(\"error_on_missing_plural\"):\n            raise e\n        else:\n            return return_value\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=61.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"python-i18n\"\nversion = \"0.3.9\"\ndescription = \"Translation library for Python\"\nreadme = \"README.md\"\nauthors = [\n    { name = \"Daniel Perez\", email = \"tuvistavie@gmail.com\" }\n]\nlicense = { file = \"LICENSE\" }\nrequires-python = \">=3.7\"\ndependencies = [\n    \"pyyaml>=3.10\"\n]\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Environment :: Other Environment\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3\",\n    \"Topic :: Software Development :: Libraries\"\n]\n\n[project.optional-dependencies]\nyaml = [\"pyyaml>=3.10\"]\n\n[tool.setuptools.packages.find]\nwhere = [\".\"]\ninclude = [\"i18n*\", \"i18n.loaders*\", \"i18n.tests*\"]\n"
  },
  {
    "path": "requirements/base.txt",
    "content": "pyyaml>=3.10\n"
  },
  {
    "path": "requirements/dev.txt",
    "content": "-r base.txt\ncoveralls\n"
  },
  {
    "path": "requirements/test.txt",
    "content": "-r dev.txt\npytest\n"
  },
  {
    "path": "requirements.txt",
    "content": "pyyaml>=3.10\ncoveralls\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup  # type: ignore\n\nsetup(\n    name=\"python-i18n\",\n    version=\"0.3.9\",\n    description=\"Translation library for Python\",\n    long_description=open(\"README.md\").read(),\n    long_description_content_type=\"text/markdown\",\n    author=\"Daniel Perez\",\n    author_email=\"tuvistavie@gmail.com\",\n    url=\"https://github.com/tuvistavie/python-i18n\",\n    download_url=\"https://github.com/tuvistavie/python-i18n/archive/master.zip\",\n    license=\"MIT\",\n    packages=[\"i18n\", \"i18n.loaders\", \"i18n.tests\"],\n    include_package_data=True,\n    zip_safe=True,\n    test_suite=\"i18n.tests\",\n    extras_require={\n        \"YAML\": [\"pyyaml>=3.10\"],\n    },\n    classifiers=[\n        \"Development Status :: 4 - Beta\",\n        \"Environment :: Other Environment\",\n        \"Intended Audience :: Developers\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n        \"Programming Language :: Python :: 3\",\n        \"Topic :: Software Development :: Libraries\",\n    ],\n)\n"
  }
]