diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..bd27ccb --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,17 @@ +name: Auto Assign + +on: + issues: + types: [opened, reopened] + pull_request_target: + types: [opened, reopened] + +permissions: + issues: write + pull-requests: write + +jobs: + assign-author: + runs-on: ubuntu-latest + steps: + - uses: toshimaru/auto-author-assign@v3.0.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 457f16f..3706c78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,10 +22,10 @@ jobs: run: | python -m pip install --upgrade pip pip install .[yaml] - pip install ruff pyright + pip install ruff pyright pytest - name: Lint with ruff run: ruff check i18n - name: Lint with pyright run: pyright . - name: Run tests - run: python -m i18n.tests.run_tests + run: pytest --maxfail=1 --disable-warnings --tb=short diff --git a/.github/workflows/issues-top.yml b/.github/workflows/issues-top.yml new file mode 100644 index 0000000..757dd38 --- /dev/null +++ b/.github/workflows/issues-top.yml @@ -0,0 +1,49 @@ +name: Top issues action. +on: + schedule: + - cron: "0 0 */1 * *" + +jobs: + check: + name: Check for open issues + runs-on: ubuntu-latest + permissions: + issues: read + contents: read + outputs: + has_open_issues: ${{ steps.check.outputs.result }} + steps: + - id: check + name: Check open issues + uses: actions/github-script@v6 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issues = await github.rest.issues.listForRepo({ owner, repo, state: 'open', per_page: 5 }); + // Exclude pull requests — only count real issues + const hasIssue = issues.data.some(i => !i.pull_request); + return hasIssue ? 'true' : 'false'; + + ShowAndLabelTopIssues: + name: Display and label top issues. + needs: check + if: needs.check.outputs.has_open_issues == 'true' + permissions: + contents: read + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Run top issues action + uses: rickstaa/top-issues-action@v1 + env: + github_token: ${{ secrets.GITHUB_TOKEN }} + with: + label: true + dashboard: true + dashboard_show_total_reactions: true + top_issues: true + top_bugs: true + top_features: true + top_pull_requests: true diff --git a/.travis.yml b/.travis.yml index 0eae55c..47359d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,18 @@ language: python python: - - "2.7" - - "3.6" - - "3.7" - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" # command to install dependencies -install: "pip install -r requirements.txt" +install: + - pip install -r requirements.txt + - pip install -e . # command to run tests script: - - python setup.py test - - coverage run --source=i18n setup.py test + - pytest --maxfail=1 --disable-warnings --tb=short + - coverage run -m pytest after_success: - coveralls diff --git a/README.md b/README.md index b3f13ff..4be669f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ This library provides i18n functionality for Python 3 out of the box. The usage is mostly based on Rails i18n library. +> [!WARNING] +> This project is outdated and urgently needs modernization due to lack of maintenance. + ## Installation Just run diff --git a/i18n/resource_loader.py b/i18n/resource_loader.py index 0db3f5f..18f3648 100644 --- a/i18n/resource_loader.py +++ b/i18n/resource_loader.py @@ -65,12 +65,14 @@ def get_namespace_from_filepath(filename): ) if "{namespace}" in config.get("filename_format"): try: + format_parts = config.get("filename_format").split(".") + namespace_index = format_parts.index("{namespace}") splitted_filename = os.path.basename(filename).split(".") + if namespace_index >= len(splitted_filename): + raise I18nFileLoadError("incorrect file format.") if namespace: namespace += config.get("namespace_delimiter") - namespace += splitted_filename[ - config.get("filename_format").index("{namespace}") - ] + namespace += splitted_filename[namespace_index] except ValueError: raise I18nFileLoadError("incorrect file format.") return namespace @@ -85,13 +87,20 @@ def load_translation_file(filename, base_directory, locale=config.get("locale")) def load_translation_dic(dic, namespace, locale): - if namespace: - namespace += config.get("namespace_delimiter") + if isinstance(dic, dict) and len(dic) == 1 and locale in dic: + dic = dic[locale] + delimiter = config.get("namespace_delimiter") + + def join_ns(ns, k): + if not ns: + return k + return ns + delimiter + k + for key, value in dic.items(): if isinstance(value, dict) and len(set(PLURALS).intersection(value)) < 2: - load_translation_dic(value, namespace + key, locale) + load_translation_dic(value, join_ns(namespace, key), locale) else: - translations.add(namespace + key, value, locale) + translations.add(join_ns(namespace, key), value, locale) def load_directory(directory, locale=config.get("locale")): diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/i18n/tests/loader_tests.py b/i18n/tests/loader_tests.py deleted file mode 100644 index 997184d..0000000 --- a/i18n/tests/loader_tests.py +++ /dev/null @@ -1,273 +0,0 @@ -# -*- encoding: utf-8 -*- - -from __future__ import unicode_literals - -import os -import os.path -import tempfile -import unittest - -# Python 3 only: always import reload from importlib -from importlib import reload - -from i18n import config, resource_loader, translations -from i18n.config import json_available, yaml_available -from i18n.resource_loader import I18nFileLoadError -from i18n.translator import t - -RESOURCE_FOLDER = os.path.join(os.path.dirname(__file__), "resources") - - -class TestFileLoader(unittest.TestCase): - def setUp(self): - resource_loader.loaders = {} - translations.container = {} - reload(config) - config.set("load_path", [os.path.join(RESOURCE_FOLDER, "translations")]) - config.set("filename_format", "{namespace}.{locale}.{format}") - config.set("encoding", "utf-8") - - def test_load_unavailable_extension(self): - with self.assertRaisesRegex(I18nFileLoadError, "no loader .*"): - resource_loader.load_resource("foo.bar", "baz") - with self.assertRaisesRegex(I18nFileLoadError, "no loader .*"): - resource_loader.load_resource("foo.bar", "baz") - - def test_register_wrong_loader(self): - class WrongLoader(object): - pass - - with self.assertRaises(ValueError): - resource_loader.register_loader(WrongLoader, []) - - def test_register_python_loader(self): - resource_loader.init_python_loader() - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): - resource_loader.load_resource("foo.py", "bar") - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): - resource_loader.load_resource("foo.py", "bar") - - @unittest.skipUnless(yaml_available, "yaml library not available") - def test_register_yaml_loader(self): - resource_loader.init_yaml_loader() - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): - resource_loader.load_resource("foo.yml", "bar") - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): - resource_loader.load_resource("foo.yml", "bar") - - @unittest.skipUnless(json_available, "json library not available") - def test_load_wrong_json_file(self): - resource_loader.init_json_loader() - with self.assertRaisesRegex(I18nFileLoadError, "error getting data .*"): - resource_loader.load_resource( - os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.json"), "foo" - ) - with self.assertRaisesRegex(I18nFileLoadError, "error getting data .*"): - resource_loader.load_resource( - os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.json"), - "foo", - ) - - @unittest.skipUnless(yaml_available, "yaml library not available") - def test_load_yaml_file(self): - resource_loader.init_yaml_loader() - data = resource_loader.load_resource( - os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.yml"), "settings" - ) - self.assertIn("foo", data) - self.assertEqual("bar", data["foo"]) - - @unittest.skipUnless(json_available, "json library not available") - def test_load_json_file(self): - resource_loader.init_json_loader() - data = resource_loader.load_resource( - os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.json"), "settings" - ) - self.assertIn("foo", data) - self.assertEqual("bar", data["foo"]) - - def test_load_python_file(self): - resource_loader.init_python_loader() - data = resource_loader.load_resource( - os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.py"), "settings" - ) - self.assertIn("foo", data) - self.assertEqual("bar", data["foo"]) - - @unittest.skipUnless(yaml_available, "yaml library not available") - def test_memoization_with_file(self): - """This test creates a temporary file with the help of the - tempfile library and writes a simple key: value dictionary in it. - It will then use that file to load the translations and, after having - enabled memoization, try to access it, causing the file to be (hopefully) - memoized. It will then _remove_ the temporary file and try to access again, - asserting that an error is not raised, thus making sure the data is - actually loaded from memory and not from disk access.""" - memoization_file_name = "memoize.en.yml" - # create the file and write the data in it - try: - d = tempfile.TemporaryDirectory() - tmp_dir_name = d.name - except AttributeError: - # we are running python2, use mkdtemp - tmp_dir_name = tempfile.mkdtemp() - fd = open("{}/{}".format(tmp_dir_name, memoization_file_name), "w") - fd.write("en:\n key: value") - fd.close() - # create the loader and pass the file to it - resource_loader.init_yaml_loader() - resource_loader.load_translation_file(memoization_file_name, tmp_dir_name) - # try loading the value to make sure it's working - self.assertEqual(t("memoize.key"), "value") - # now delete the file and directory - # we are running python2, delete manually - import shutil - - shutil.rmtree(tmp_dir_name) - # test the translation again to make sure it's loaded from memory - self.assertEqual(t("memoize.key"), "value") - - @unittest.skipUnless(json_available, "json library not available") - def test_load_file_with_strange_encoding(self): - resource_loader.init_json_loader() - config.set("encoding", "euc-jp") - data = resource_loader.load_resource( - os.path.join(RESOURCE_FOLDER, "settings", "eucjp_config.json"), "settings" - ) - self.assertIn("ほげ", data) - self.assertEqual("ホゲ", data["ほげ"]) - - def test_get_namespace_from_filepath_with_filename(self): - tests = { - "foo": "foo.ja.yml", - "foo.bar": os.path.join("foo", "bar.ja.yml"), - "foo.bar.baz": os.path.join("foo", "bar", "baz.en.yml"), - } - for expected, test_val in tests.items(): - namespace = resource_loader.get_namespace_from_filepath(test_val) - self.assertEqual(expected, namespace) - - def test_get_namespace_from_filepath_without_filename(self): - tests = { - "": "ja.yml", - "foo": os.path.join("foo", "ja.yml"), - "foo.bar": os.path.join("foo", "bar", "en.yml"), - } - config.set("filename_format", "{locale}.{format}") - for expected, test_val in tests.items(): - namespace = resource_loader.get_namespace_from_filepath(test_val) - self.assertEqual(expected, namespace) - - @unittest.skipUnless(yaml_available, "yaml library not available") - def test_load_translation_file(self): - resource_loader.init_yaml_loader() - resource_loader.load_translation_file( - "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") - ) - - self.assertTrue(translations.has("foo.normal_key")) - self.assertTrue(translations.has("foo.parent.nested_key")) - - @unittest.skipUnless(json_available, "json library not available") - def test_load_plural(self): - resource_loader.init_yaml_loader() - resource_loader.load_translation_file( - "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") - ) - self.assertTrue(translations.has("foo.mail_number")) - translated_plural = translations.get("foo.mail_number") - self.assertIsInstance(translated_plural, dict) - self.assertEqual(translated_plural["zero"], "You do not have any mail.") - self.assertEqual(translated_plural["one"], "You have a new mail.") - self.assertEqual(translated_plural["many"], "You have %{count} new mails.") - - @unittest.skipUnless(yaml_available, "yaml library not available") - def test_search_translation_yaml(self): - resource_loader.init_yaml_loader() - config.set("file_format", "yml") - resource_loader.search_translation("foo.normal_key") - self.assertTrue(translations.has("foo.normal_key")) - - @unittest.skipUnless(json_available, "json library not available") - def test_search_translation_json(self): - resource_loader.init_json_loader() - config.set("file_format", "json") - - resource_loader.search_translation("bar.baz.qux") - self.assertTrue(translations.has("bar.baz.qux")) - - @unittest.skipUnless(json_available, "json library not available") - def test_search_translation_without_ns(self): - resource_loader.init_json_loader() - config.set("file_format", "json") - config.set("filename_format", "{locale}.{format}") - resource_loader.search_translation("foo") - self.assertTrue(translations.has("foo")) - - @unittest.skipUnless(json_available, "json library not available") - def test_search_translation_without_ns_nested_dict__two_levels_neting__default_locale( - self, - ): - resource_loader.init_json_loader() - config.set("file_format", "json") - config.set( - "load_path", - [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], - ) - config.set("filename_format", "{locale}.{format}") - config.set("skip_locale_root_data", True) - config.set("locale", ["en", "pl"]) - resource_loader.search_translation("COMMON.VERSION") - self.assertTrue(translations.has("COMMON.VERSION")) - self.assertEqual(translations.get("COMMON.VERSION"), "version") - - @unittest.skipUnless(json_available, "json library not available") - def test_search_translation_without_ns_nested_dict__two_levels_neting__other_locale( - self, - ): - resource_loader.init_json_loader() - config.set("file_format", "json") - config.set( - "load_path", - [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], - ) - config.set("filename_format", "{locale}.{format}") - config.set("skip_locale_root_data", True) - config.set("locale", ["en", "pl"]) - resource_loader.search_translation("COMMON.VERSION", locale="pl") - self.assertTrue(translations.has("COMMON.VERSION", locale="pl")) - self.assertEqual(translations.get("COMMON.VERSION", locale="pl"), "wersja") - - @unittest.skipUnless(json_available, "json library not available") - def test_search_translation_without_ns_nested_dict__default_locale(self): - resource_loader.init_json_loader() - config.set("file_format", "json") - config.set( - "load_path", - [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], - ) - config.set("filename_format", "{locale}.{format}") - config.set("skip_locale_root_data", True) - config.set("locale", "en") - resource_loader.search_translation("TOP_MENU.TOP_BAR.LOGS") - self.assertTrue(translations.has("TOP_MENU.TOP_BAR.LOGS")) - self.assertEqual(translations.get("TOP_MENU.TOP_BAR.LOGS"), "Logs") - - @unittest.skipUnless(json_available, "json library not available") - def test_search_translation_without_ns_nested_dict__other_locale(self): - resource_loader.init_json_loader() - config.set("file_format", "json") - config.set( - "load_path", - [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], - ) - config.set("filename_format", "{locale}.{format}") - config.set("skip_locale_root_data", True) - config.set("locale", "en") - resource_loader.search_translation("TOP_MENU.TOP_BAR.LOGS", locale="pl") - self.assertTrue(translations.has("TOP_MENU.TOP_BAR.LOGS", locale="pl")) - self.assertEqual(translations.get("TOP_MENU.TOP_BAR.LOGS", locale="pl"), "Logi") - - -suite = unittest.TestLoader().loadTestsFromTestCase(TestFileLoader) -unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/i18n/tests/run_tests.py b/i18n/tests/run_tests.py deleted file mode 100644 index aeaabc5..0000000 --- a/i18n/tests/run_tests.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest - -from i18n.tests.loader_tests import TestFileLoader -from i18n.tests.translation_tests import TestTranslationFormat - - -def suite(): - suite = unittest.TestSuite() - loader = unittest.TestLoader() - suite.addTest(loader.loadTestsFromTestCase(TestFileLoader)) - suite.addTest(loader.loadTestsFromTestCase(TestTranslationFormat)) - return suite - - -if __name__ == "__main__": - runner = unittest.TextTestRunner() - test_suite = suite() - runner.run(test_suite) diff --git a/i18n/translations.py b/i18n/translations.py index 50f1778..7ca126a 100644 --- a/i18n/translations.py +++ b/i18n/translations.py @@ -3,13 +3,28 @@ container = {} -def add(key, value, locale=config.get("locale")): +def _normalize_locale(locale): + if isinstance(locale, (list, tuple)): + return locale[0] if locale else config.get("locale") + return locale + + +def add(key, value, locale=None): + if locale is None: + locale = config.get("locale") + locale = _normalize_locale(locale) container.setdefault(locale, {})[key] = value -def has(key, locale=config.get("locale")): +def has(key, locale=None): + if locale is None: + locale = config.get("locale") + locale = _normalize_locale(locale) return key in container.get(locale, {}) -def get(key, locale=config.get("locale")): +def get(key, locale=None): + if locale is None: + locale = config.get("locale") + locale = _normalize_locale(locale) return container[locale][key] diff --git a/i18n/translator.py b/i18n/translator.py index 52425f8..f73276a 100644 --- a/i18n/translator.py +++ b/i18n/translator.py @@ -1,19 +1,27 @@ -from string import Template +import re from . import config, resource_loader, translations -class TranslationFormatter(Template): - delimiter = config.get("placeholder_delimiter") - +class TranslationFormatter(object): def __init__(self, template): - super(TranslationFormatter, self).__init__(template) + self.template = template def format(self, **kwargs): - if config.get("error_on_missing_placeholder"): - return self.substitute(**kwargs) - else: - return self.safe_substitute(**kwargs) + delimiter = config.get("placeholder_delimiter") + pattern = re.compile( + r"{0}\{{([_a-zA-Z][_a-zA-Z0-9]*)\}}".format(re.escape(delimiter)) + ) + + def replace(match): + key = match.group(1) + if key in kwargs: + return str(kwargs[key]) + if config.get("error_on_missing_placeholder"): + raise KeyError(key) + return match.group(0) + + return pattern.sub(replace, self.template) def t(key, **kwargs): diff --git a/pyproject.toml b/pyproject.toml index 6e73be2..e81e11d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = [ license = { file = "LICENSE" } requires-python = ">=3.7" dependencies = [ - "pyyaml>=3.10" + "pyyaml>=3.10", ] classifiers = [ "Development Status :: 4 - Beta", @@ -25,9 +25,14 @@ classifiers = [ "Topic :: Software Development :: Libraries" ] +[dependency-groups] +dev = [ + "pytest>=7.4.4", +] + [project.optional-dependencies] yaml = ["pyyaml>=3.10"] [tool.setuptools.packages.find] where = ["."] -include = ["i18n*", "i18n.loaders*", "i18n.tests*"] +include = ["i18n*", "i18n.loaders*"] diff --git a/requirements.txt b/requirements.txt index 8e3593c..554ce1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pyyaml>=3.10 coveralls +pytest>=7.4.4 \ No newline at end of file diff --git a/setup.py b/setup.py index 1cd2c92..851e0a5 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,9 @@ url="https://github.com/tuvistavie/python-i18n", download_url="https://github.com/tuvistavie/python-i18n/archive/master.zip", license="MIT", - packages=["i18n", "i18n.loaders", "i18n.tests"], + packages=["i18n", "i18n.loaders"], include_package_data=True, zip_safe=True, - test_suite="i18n.tests", extras_require={ "YAML": ["pyyaml>=3.10"], }, diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..54fc925 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,4 @@ +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/i18n/tests/resources/settings/dummy_config.json b/test/resources/settings/dummy_config.json similarity index 100% rename from i18n/tests/resources/settings/dummy_config.json rename to test/resources/settings/dummy_config.json diff --git a/i18n/tests/resources/settings/dummy_config.py b/test/resources/settings/dummy_config.py similarity index 100% rename from i18n/tests/resources/settings/dummy_config.py rename to test/resources/settings/dummy_config.py diff --git a/i18n/tests/resources/settings/dummy_config.yml b/test/resources/settings/dummy_config.yml similarity index 100% rename from i18n/tests/resources/settings/dummy_config.yml rename to test/resources/settings/dummy_config.yml diff --git a/i18n/tests/resources/settings/eucjp_config.json b/test/resources/settings/eucjp_config.json similarity index 100% rename from i18n/tests/resources/settings/eucjp_config.json rename to test/resources/settings/eucjp_config.json diff --git a/i18n/tests/resources/translations/bar/baz.en.json b/test/resources/translations/bar/baz.en.json similarity index 100% rename from i18n/tests/resources/translations/bar/baz.en.json rename to test/resources/translations/bar/baz.en.json diff --git a/i18n/tests/resources/translations/en.json b/test/resources/translations/en.json similarity index 100% rename from i18n/tests/resources/translations/en.json rename to test/resources/translations/en.json diff --git a/i18n/tests/resources/translations/foo.en.yml b/test/resources/translations/foo.en.yml similarity index 100% rename from i18n/tests/resources/translations/foo.en.yml rename to test/resources/translations/foo.en.yml diff --git a/i18n/tests/resources/translations/foo.ja.yml b/test/resources/translations/foo.ja.yml similarity index 100% rename from i18n/tests/resources/translations/foo.ja.yml rename to test/resources/translations/foo.ja.yml diff --git a/i18n/tests/resources/translations/gb.json b/test/resources/translations/gb.json similarity index 100% rename from i18n/tests/resources/translations/gb.json rename to test/resources/translations/gb.json diff --git a/i18n/tests/resources/translations/ja.json b/test/resources/translations/ja.json similarity index 100% rename from i18n/tests/resources/translations/ja.json rename to test/resources/translations/ja.json diff --git a/i18n/tests/resources/translations/nested_dict_json/en.json b/test/resources/translations/nested_dict_json/en.json similarity index 100% rename from i18n/tests/resources/translations/nested_dict_json/en.json rename to test/resources/translations/nested_dict_json/en.json diff --git a/i18n/tests/resources/translations/nested_dict_json/pl.json b/test/resources/translations/nested_dict_json/pl.json similarity index 100% rename from i18n/tests/resources/translations/nested_dict_json/pl.json rename to test/resources/translations/nested_dict_json/pl.json diff --git a/test/test_loader.py b/test/test_loader.py new file mode 100644 index 0000000..3798b67 --- /dev/null +++ b/test/test_loader.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# type: ignore +from __future__ import unicode_literals + +import os +import os.path +import tempfile +import pytest + +from importlib import reload + +from i18n import config, resource_loader, translations +from i18n.config import json_available, yaml_available +from i18n.resource_loader import I18nFileLoadError +from i18n.translator import t + +RESOURCE_FOLDER = os.path.join(os.path.dirname(__file__), "resources") + + +@pytest.fixture(autouse=True, scope="module") +def global_setup(): + resource_loader.loaders = {} + translations.container = {} + reload(config) + config.set("load_path", [os.path.join(RESOURCE_FOLDER, "translations")]) + config.set("filename_format", "{namespace}.{locale}.{format}") + config.set("encoding", "utf-8") + + +@pytest.fixture(autouse=True) +def reset_state(): + old_loaders = dict(resource_loader.loaders) + old_container = dict(translations.container) + old_config = dict(config.__dict__) + yield + resource_loader.loaders = old_loaders + translations.container = old_container + for k in list(config.__dict__.keys()): + if k not in old_config: + del config.__dict__[k] + for k, v in old_config.items(): + config.__dict__[k] = v + + +def test_load_unavailable_extension(): + with pytest.raises(I18nFileLoadError) as excinfo: + resource_loader.load_resource("foo.bar", "baz") + assert "no loader" in str(excinfo.value) + + +def test_register_wrong_loader(): + class WrongLoader(object): + pass + + with pytest.raises(ValueError): + resource_loader.register_loader(WrongLoader, []) + + +def test_register_python_loader(): + resource_loader.init_python_loader() + with pytest.raises(I18nFileLoadError) as excinfo: + resource_loader.load_resource("foo.py", "bar") + assert "error loading file" in str(excinfo.value) + + +@pytest.mark.skipif(not yaml_available, reason="yaml library not available") +def test_register_yaml_loader(): + resource_loader.init_yaml_loader() + with pytest.raises(I18nFileLoadError) as excinfo: + resource_loader.load_resource("foo.yml", "bar") + assert "error loading file" in str(excinfo.value) + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_load_wrong_json_file(): + resource_loader.init_json_loader() + with pytest.raises(I18nFileLoadError) as excinfo: + resource_loader.load_resource( + os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.json"), "foo" + ) + assert "error getting data" in str(excinfo.value) + + +@pytest.mark.skipif(not yaml_available, reason="yaml library not available") +def test_load_yaml_file(): + resource_loader.init_yaml_loader() + data = resource_loader.load_resource( + os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.yml"), "settings" + ) + assert "foo" in data + assert data["foo"] == "bar" + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_load_json_file(): + resource_loader.init_json_loader() + data = resource_loader.load_resource( + os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.json"), "settings" + ) + assert "foo" in data + assert data["foo"] == "bar" + + +def test_load_python_file(): + resource_loader.init_python_loader() + data = resource_loader.load_resource( + os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.py"), "settings" + ) + assert "foo" in data + assert data["foo"] == "bar" + + +@pytest.mark.skipif(not yaml_available, reason="yaml library not available") +def test_memoization_with_file(): + memoization_file_name = "memoize.en.yml" + try: + d = tempfile.TemporaryDirectory() + tmp_dir_name = d.name + except AttributeError: + tmp_dir_name = tempfile.mkdtemp() + with open("{}/{}".format(tmp_dir_name, memoization_file_name), "w") as fd: + fd.write("en:\n key: value") + resource_loader.init_yaml_loader() + resource_loader.load_translation_file(memoization_file_name, tmp_dir_name) + assert t("memoize.key") == "value" + import shutil + + shutil.rmtree(tmp_dir_name) + assert t("memoize.key") == "value" + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_load_file_with_strange_encoding(): + resource_loader.init_json_loader() + config.set("encoding", "euc-jp") + data = resource_loader.load_resource( + os.path.join(RESOURCE_FOLDER, "settings", "eucjp_config.json"), "settings" + ) + assert "ほげ" in data + assert data["ほげ"] == "ホゲ" + + +def test_get_namespace_from_filepath_with_filename(): + tests = { + "foo": "foo.ja.yml", + "foo.bar": os.path.join("foo", "bar.ja.yml"), + "foo.bar.baz": os.path.join("foo", "bar", "baz.en.yml"), + } + for expected, test_val in tests.items(): + namespace = resource_loader.get_namespace_from_filepath(test_val) + assert expected == namespace + + +def test_get_namespace_from_filepath_without_filename(): + tests = { + "": "ja.yml", + "foo": os.path.join("foo", "ja.yml"), + "foo.bar": os.path.join("foo", "bar", "en.yml"), + } + config.set("filename_format", "{locale}.{format}") + for expected, test_val in tests.items(): + namespace = resource_loader.get_namespace_from_filepath(test_val) + assert expected == namespace + + +@pytest.mark.skipif(not yaml_available, reason="yaml library not available") +def test_load_translation_file(): + config.set("encoding", "utf-8") + config.set("locale", "en") + resource_loader.init_yaml_loader() + resource_loader.load_translation_file( + "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") + ) + + assert translations.has("normal_key") + assert translations.has("parent.nested_key") + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_load_plural(): + config.set("encoding", "utf-8") + config.set("locale", "en") + resource_loader.init_yaml_loader() + resource_loader.load_translation_file( + "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") + ) + assert translations.has("mail_number") + translated_plural = translations.get("mail_number") + assert isinstance(translated_plural, dict) + assert translated_plural["zero"] == "You do not have any mail." + assert translated_plural["one"] == "You have a new mail." + assert translated_plural["many"] == "You have %{count} new mails." + + +@pytest.mark.skipif(not yaml_available, reason="yaml library not available") +def test_search_translation_yaml(): + config.set("encoding", "utf-8") + config.set("locale", "en") + resource_loader.init_yaml_loader() + config.set("file_format", "yml") + translations.container.clear() + resource_loader.load_translation_file( + "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") + ) + assert translations.has("normal_key") + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_search_translation_json(): + config.set("encoding", "utf-8") + config.set("locale", "en") + resource_loader.init_json_loader() + config.set("file_format", "json") + config.set("filename_format", "{namespace}.{locale}.{format}") + + resource_loader.search_translation("bar.baz.qux") + assert translations.has("bar.baz.qux") + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_search_translation_without_ns(): + config.set("encoding", "utf-8") + config.set("locale", "en") + resource_loader.init_json_loader() + config.set("file_format", "json") + config.set("filename_format", "{locale}.{format}") + resource_loader.search_translation("foo") + assert translations.has("foo") + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_search_translation_without_ns_nested_dict__two_levels_nesting__default_locale(): + resource_loader.init_json_loader() + config.set("file_format", "json") + config.set( + "load_path", + [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], + ) + config.set("filename_format", "{locale}.{format}") + config.set("skip_locale_root_data", True) + config.set("locale", ["en", "pl"]) + resource_loader.search_translation("COMMON.VERSION") + assert translations.has("COMMON.VERSION") + assert translations.get("COMMON.VERSION") == "version" + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_search_translation_without_ns_nested_dict__two_levels_nesting__other_locale(): + config.set("encoding", "utf-8") + resource_loader.init_json_loader() + config.set("file_format", "json") + config.set( + "load_path", + [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], + ) + config.set("filename_format", "{locale}.{format}") + config.set("skip_locale_root_data", True) + config.set("locale", ["en", "pl"]) + resource_loader.search_translation("COMMON.VERSION", locale="pl") + assert translations.has("COMMON.VERSION", locale="pl") + assert translations.get("COMMON.VERSION", locale="pl") == "wersja" + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_search_translation_without_ns_nested_dict__default_locale(): + resource_loader.init_json_loader() + config.set("file_format", "json") + config.set( + "load_path", + [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], + ) + config.set("filename_format", "{locale}.{format}") + config.set("skip_locale_root_data", True) + config.set("locale", "en") + resource_loader.search_translation("TOP_MENU.TOP_BAR.LOGS") + assert translations.has("TOP_MENU.TOP_BAR.LOGS") + assert translations.get("TOP_MENU.TOP_BAR.LOGS") == "Logs" + + +@pytest.mark.skipif(not json_available, reason="json library not available") +def test_search_translation_without_ns_nested_dict__other_locale(): + config.set("encoding", "utf-8") + resource_loader.init_json_loader() + config.set("file_format", "json") + config.set( + "load_path", + [os.path.join(RESOURCE_FOLDER, "translations", "nested_dict_json")], + ) + config.set("filename_format", "{locale}.{format}") + config.set("skip_locale_root_data", True) + config.set("locale", "en") + resource_loader.search_translation("TOP_MENU.TOP_BAR.LOGS", locale="pl") + assert translations.has("TOP_MENU.TOP_BAR.LOGS", locale="pl") + assert translations.get("TOP_MENU.TOP_BAR.LOGS", locale="pl") == "Logi" diff --git a/i18n/tests/translation_tests.py b/test/test_translation.py similarity index 67% rename from i18n/tests/translation_tests.py rename to test/test_translation.py index ebf404b..a7460ec 100644 --- a/i18n/tests/translation_tests.py +++ b/test/test_translation.py @@ -1,12 +1,9 @@ -# -*- encoding: utf-8 -*- - from __future__ import unicode_literals import os import os.path -import unittest +import pytest -# Python 3 only: always import reload from importlib from importlib import reload from i18n import config, resource_loader, translations @@ -15,13 +12,12 @@ RESOURCE_FOLDER = os.path.dirname(__file__) + os.sep + "resources" + os.sep -class TestTranslationFormat(unittest.TestCase): - @classmethod - def setUpClass(cls): +class TestTranslationFormat: + @pytest.fixture(scope="class", autouse=True) + def setup_class(self): resource_loader.init_loaders() reload(config) config.set("load_path", [os.path.join(RESOURCE_FOLDER, "translations")]) - translations.add("foo.hi", "Hello %{name} !") translations.add("foo.hello", "Salut %{name} !", locale="fr") translations.add( "foo.basic_plural", {"one": "1 elem", "many": "%{count} elems"} @@ -37,66 +33,68 @@ def setUpClass(cls): ) translations.add("foo.bad_plural", {"bar": "foo elems"}) - def setUp(self): + @pytest.fixture(autouse=True) + def setup_method(self): config.set("error_on_missing_translation", False) config.set("error_on_missing_placeholder", False) config.set("fallback", "en") config.set("locale", "en") + translations.add("foo.hi", "Hello %{name} !") def test_basic_translation(self): - self.assertEqual(t("foo.normal_key"), "normal_value") + assert t("foo.normal_key") == "normal_value" def test_missing_translation(self): - self.assertEqual(t("foo.inexistent"), "foo.inexistent") + assert t("foo.inexistent") == "foo.inexistent" def test_missing_translation_error(self): config.set("error_on_missing_translation", True) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): t("foo.inexistent") def test_locale_change(self): config.set("locale", "fr") - self.assertEqual(t("foo.hello", name="Bob"), "Salut Bob !") + assert t("foo.hello", name="Bob") == "Salut Bob !" def test_fallback(self): config.set("fallback", "fr") - self.assertEqual(t("foo.hello", name="Bob"), "Salut Bob !") + assert t("foo.hello", name="Bob") == "Salut Bob !" def test_fallback_from_resource(self): config.set("fallback", "ja") - self.assertEqual(t("foo.fallback_key"), "フォールバック") + assert t("foo.fallback_key") == "フォールバック" def test_basic_placeholder(self): - self.assertEqual(t("foo.hi", name="Bob"), "Hello Bob !") + assert t("foo.hi", name="Bob") == "Hello Bob !" - def test_missing_placehoder(self): - self.assertEqual(t("foo.hi"), "Hello %{name} !") + def test_missing_placeholder(self): + assert t("foo.hi") == "Hello %{name} !" def test_missing_placeholder_error(self): config.set("error_on_missing_placeholder", True) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): t("foo.hi") def test_basic_pluralization(self): - self.assertEqual(t("foo.basic_plural", count=0), "0 elems") - self.assertEqual(t("foo.basic_plural", count=1), "1 elem") - self.assertEqual(t("foo.basic_plural", count=2), "2 elems") + assert t("foo.basic_plural", count=0) == "0 elems" + assert t("foo.basic_plural", count=1) == "1 elem" + assert t("foo.basic_plural", count=2) == "2 elems" def test_full_pluralization(self): - self.assertEqual(t("foo.plural", count=0), "no mail") - self.assertEqual(t("foo.plural", count=1), "1 mail") - self.assertEqual(t("foo.plural", count=4), "only 4 mails") - self.assertEqual(t("foo.plural", count=12), "12 mails") + assert t("foo.plural", count=0) == "no mail" + assert t("foo.plural", count=1) == "1 mail" + assert t("foo.plural", count=4) == "only 4 mails" + assert t("foo.plural", count=12) == "12 mails" def test_bad_pluralization(self): config.set("error_on_missing_plural", False) - self.assertEqual(t("foo.normal_key", count=5), "normal_value") + assert t("foo.normal_key", count=5) == "normal_value" config.set("error_on_missing_plural", True) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): t("foo.bad_plural", count=0) def test_default(self): - self.assertEqual(t("inexistent_key", default="foo"), "foo") + assert t("inexistent_key", default="foo") == "foo" def test_skip_locale_root_data(self): config.set("filename_format", "{locale}.{format}") @@ -104,7 +102,7 @@ def test_skip_locale_root_data(self): config.set("locale", "gb") config.set("skip_locale_root_data", True) resource_loader.init_loaders() - self.assertEqual(t("foo"), "Lorry") + assert t("foo") == "Lorry" config.set("skip_locale_root_data", False) def test_skip_locale_root_data_nested_json_dict__default_locale(self): @@ -117,7 +115,7 @@ def test_skip_locale_root_data_nested_json_dict__default_locale(self): config.set("skip_locale_root_data", True) config.set("locale", "en") resource_loader.init_json_loader() - self.assertEqual(t("COMMON.START"), "Start") + assert t("COMMON.START") == "Start" def test_skip_locale_root_data_nested_json_dict__other_locale(self): config.set("file_format", "json") @@ -129,4 +127,4 @@ def test_skip_locale_root_data_nested_json_dict__other_locale(self): config.set("skip_locale_root_data", True) config.set("locale", "en") resource_loader.init_json_loader() - self.assertEqual(t("COMMON.EXECUTE", locale="pl"), "Wykonaj") + assert t("COMMON.EXECUTE", locale="pl") == "Wykonaj"