From 8286ee71fa3b44da35bb1d11257c5a51195d3f3e Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 03:41:20 +0800 Subject: [PATCH 01/20] feat: Add translation tests import to test suite --- i18n/tests/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py index e69de29..3689747 100644 --- a/i18n/tests/__init__.py +++ b/i18n/tests/__init__.py @@ -0,0 +1,2 @@ + +from . import translation_tests From 0f52666a571bf75de7e080f06d45f8459d24975e Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 03:41:47 +0800 Subject: [PATCH 02/20] refactor: Remove unnecessary blank line in test suite initialization --- i18n/tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py index 3689747..5a9cb31 100644 --- a/i18n/tests/__init__.py +++ b/i18n/tests/__init__.py @@ -1,2 +1 @@ - from . import translation_tests From 696a9c27cf7fdbc12947f07ac985952b5cff646f Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 04:23:08 +0800 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=A1=86=E6=9E=B6=E8=87=B3pytest=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 4 +- i18n/tests/__init__.py | 2 +- i18n/tests/loader_tests.py | 113 +++++++++++++++++--------------- i18n/tests/run_tests.py | 19 +----- i18n/tests/translation_tests.py | 58 ++++++++-------- pyproject.toml | 3 +- setup.py | 2 +- 7 files changed, 97 insertions(+), 104 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0eae55c..33ee43a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: install: "pip install -r requirements.txt" # 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/i18n/tests/__init__.py b/i18n/tests/__init__.py index 5a9cb31..5851165 100644 --- a/i18n/tests/__init__.py +++ b/i18n/tests/__init__.py @@ -1 +1 @@ -from . import translation_tests +from . import translation_tests # noqa: F401 diff --git a/i18n/tests/loader_tests.py b/i18n/tests/loader_tests.py index 997184d..f81f221 100644 --- a/i18n/tests/loader_tests.py +++ b/i18n/tests/loader_tests.py @@ -1,3 +1,17 @@ +""" +loader_tests.py +---------------- +本文件包含对 i18n.loaders 目录下各类资源加载器的单元测试。 +主要测试 JSON、YAML、Python 格式的配置和翻译文件的加载功能, +确保各类 loader 能正确解析和返回预期的数据结构。 + +测试覆盖点: +- JSONLoader、YAMLLoader、PythonLoader 的基本功能 +- 各类配置文件的加载与异常处理 +- 边界情况与错误输入的健壮性 + +依赖:pytest +""" # -*- encoding: utf-8 -*- from __future__ import unicode_literals @@ -6,6 +20,7 @@ import os.path import tempfile import unittest +import pytest # Python 3 only: always import reload from importlib from importlib import reload @@ -18,8 +33,9 @@ RESOURCE_FOLDER = os.path.join(os.path.dirname(__file__), "resources") -class TestFileLoader(unittest.TestCase): - def setUp(self): +class TestFileLoader: + @pytest.fixture(autouse=True) + def setup_method(self): resource_loader.loaders = {} translations.container = {} reload(config) @@ -28,45 +44,38 @@ def setUp(self): config.set("encoding", "utf-8") def test_load_unavailable_extension(self): - with self.assertRaisesRegex(I18nFileLoadError, "no loader .*"): + with pytest.raises(I18nFileLoadError) as excinfo: resource_loader.load_resource("foo.bar", "baz") - with self.assertRaisesRegex(I18nFileLoadError, "no loader .*"): - resource_loader.load_resource("foo.bar", "baz") + assert "no loader" in str(excinfo.value) def test_register_wrong_loader(self): class WrongLoader(object): pass - with self.assertRaises(ValueError): + with pytest.raises(ValueError): resource_loader.register_loader(WrongLoader, []) def test_register_python_loader(self): resource_loader.init_python_loader() - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): + with pytest.raises(I18nFileLoadError) as excinfo: resource_loader.load_resource("foo.py", "bar") - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): - resource_loader.load_resource("foo.py", "bar") + assert "error loading file" in str(excinfo.value) - @unittest.skipUnless(yaml_available, "yaml library not available") + @pytest.mark.skipif(not yaml_available, reason="yaml library not available") def test_register_yaml_loader(self): resource_loader.init_yaml_loader() - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): + with pytest.raises(I18nFileLoadError) as excinfo: resource_loader.load_resource("foo.yml", "bar") - with self.assertRaisesRegex(I18nFileLoadError, "error loading file .*"): - resource_loader.load_resource("foo.yml", "bar") + assert "error loading file" in str(excinfo.value) - @unittest.skipUnless(json_available, "json library not available") + @pytest.mark.skipif(not json_available, reason="json library not available") def test_load_wrong_json_file(self): resource_loader.init_json_loader() - with self.assertRaisesRegex(I18nFileLoadError, "error getting data .*"): + with pytest.raises(I18nFileLoadError) as excinfo: 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", - ) + assert "error getting data" in str(excinfo.value) @unittest.skipUnless(yaml_available, "yaml library not available") def test_load_yaml_file(self): @@ -74,8 +83,8 @@ def test_load_yaml_file(self): data = resource_loader.load_resource( os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.yml"), "settings" ) - self.assertIn("foo", data) - self.assertEqual("bar", data["foo"]) + assert "foo" in data + assert data["foo"] == "bar" @unittest.skipUnless(json_available, "json library not available") def test_load_json_file(self): @@ -83,16 +92,16 @@ def test_load_json_file(self): data = resource_loader.load_resource( os.path.join(RESOURCE_FOLDER, "settings", "dummy_config.json"), "settings" ) - self.assertIn("foo", data) - self.assertEqual("bar", data["foo"]) + assert "foo" in data + assert data["foo"] == "bar" 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"]) + assert "foo" in data + assert data["foo"] == "bar" @unittest.skipUnless(yaml_available, "yaml library not available") def test_memoization_with_file(self): @@ -118,14 +127,14 @@ def test_memoization_with_file(self): 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") + assert 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") + assert t("memoize.key") == "value" @unittest.skipUnless(json_available, "json library not available") def test_load_file_with_strange_encoding(self): @@ -134,8 +143,8 @@ def test_load_file_with_strange_encoding(self): data = resource_loader.load_resource( os.path.join(RESOURCE_FOLDER, "settings", "eucjp_config.json"), "settings" ) - self.assertIn("ほげ", data) - self.assertEqual("ホゲ", data["ほげ"]) + assert "ほげ" in data + assert data["ほげ"] == "ホゲ" def test_get_namespace_from_filepath_with_filename(self): tests = { @@ -145,7 +154,7 @@ def test_get_namespace_from_filepath_with_filename(self): } for expected, test_val in tests.items(): namespace = resource_loader.get_namespace_from_filepath(test_val) - self.assertEqual(expected, namespace) + assert expected == namespace def test_get_namespace_from_filepath_without_filename(self): tests = { @@ -156,7 +165,7 @@ def test_get_namespace_from_filepath_without_filename(self): 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) + assert expected == namespace @unittest.skipUnless(yaml_available, "yaml library not available") def test_load_translation_file(self): @@ -165,8 +174,8 @@ def test_load_translation_file(self): "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") ) - self.assertTrue(translations.has("foo.normal_key")) - self.assertTrue(translations.has("foo.parent.nested_key")) + assert translations.has("foo.normal_key") + assert translations.has("foo.parent.nested_key") @unittest.skipUnless(json_available, "json library not available") def test_load_plural(self): @@ -174,19 +183,19 @@ def test_load_plural(self): resource_loader.load_translation_file( "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") ) - self.assertTrue(translations.has("foo.mail_number")) + assert 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.") + 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." @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")) + assert translations.has("foo.normal_key") @unittest.skipUnless(json_available, "json library not available") def test_search_translation_json(self): @@ -194,7 +203,7 @@ def test_search_translation_json(self): config.set("file_format", "json") resource_loader.search_translation("bar.baz.qux") - self.assertTrue(translations.has("bar.baz.qux")) + assert translations.has("bar.baz.qux") @unittest.skipUnless(json_available, "json library not available") def test_search_translation_without_ns(self): @@ -202,7 +211,7 @@ def test_search_translation_without_ns(self): config.set("file_format", "json") config.set("filename_format", "{locale}.{format}") resource_loader.search_translation("foo") - self.assertTrue(translations.has("foo")) + assert translations.has("foo") @unittest.skipUnless(json_available, "json library not available") def test_search_translation_without_ns_nested_dict__two_levels_neting__default_locale( @@ -218,8 +227,8 @@ def test_search_translation_without_ns_nested_dict__two_levels_neting__default_l 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") + assert translations.has("COMMON.VERSION") + assert 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( @@ -235,8 +244,8 @@ def test_search_translation_without_ns_nested_dict__two_levels_neting__other_loc 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") + assert translations.has("COMMON.VERSION", locale="pl") + assert 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): @@ -250,8 +259,8 @@ def test_search_translation_without_ns_nested_dict__default_locale(self): 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") + assert translations.has("TOP_MENU.TOP_BAR.LOGS") + assert 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): @@ -265,9 +274,5 @@ def test_search_translation_without_ns_nested_dict__other_locale(self): 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) + 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/run_tests.py b/i18n/tests/run_tests.py index aeaabc5..e1ff2d9 100644 --- a/i18n/tests/run_tests.py +++ b/i18n/tests/run_tests.py @@ -1,18 +1 @@ -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) +# 此文件已废弃,迁移到 pytest 后无需此入口。 diff --git a/i18n/tests/translation_tests.py b/i18n/tests/translation_tests.py index ebf404b..436fd83 100644 --- a/i18n/tests/translation_tests.py +++ b/i18n/tests/translation_tests.py @@ -4,7 +4,7 @@ import os import os.path -import unittest +import pytest # Python 3 only: always import reload from importlib from importlib import reload @@ -15,9 +15,9 @@ 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(cls): resource_loader.init_loaders() reload(config) config.set("load_path", [os.path.join(RESOURCE_FOLDER, "translations")]) @@ -37,66 +37,70 @@ 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): + import pytest + + 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} !") + 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 +108,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 +121,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 +133,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" diff --git a/pyproject.toml b/pyproject.toml index 6e73be2..dc55a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ authors = [ license = { file = "LICENSE" } requires-python = ">=3.7" dependencies = [ - "pyyaml>=3.10" + "pytest>=7.4.4", + "pyyaml>=3.10", ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/setup.py b/setup.py index 1cd2c92..8a85ba5 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ packages=["i18n", "i18n.loaders", "i18n.tests"], include_package_data=True, zip_safe=True, - test_suite="i18n.tests", + # test_suite removed for pytest migration extras_require={ "YAML": ["pyyaml>=3.10"], }, From b3743b556c56c809e99766ef3e1b7c3e81427037 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 04:36:33 +0800 Subject: [PATCH 04/20] Refactor i18n tests: remove deprecated files and migrate to new test structure - Deleted old test files: `__init__.py`, `loader_tests.py`, `translation_tests.py`, and `run_tests.py`. - Removed associated resource files for settings and translations. - Created new test files in the `test` directory to maintain the same test coverage. - Ensured all necessary resources are replicated in the new structure for continued functionality. --- .travis.yml | 8 +++++--- i18n/tests/__init__.py | 1 - i18n/tests/run_tests.py | 1 - {i18n/tests => test}/resources/settings/dummy_config.json | 0 {i18n/tests => test}/resources/settings/dummy_config.py | 0 {i18n/tests => test}/resources/settings/dummy_config.yml | 0 {i18n/tests => test}/resources/settings/eucjp_config.json | 0 .../tests => test}/resources/translations/bar/baz.en.json | 0 {i18n/tests => test}/resources/translations/en.json | 0 {i18n/tests => test}/resources/translations/foo.en.yml | 0 {i18n/tests => test}/resources/translations/foo.ja.yml | 0 {i18n/tests => test}/resources/translations/gb.json | 0 {i18n/tests => test}/resources/translations/ja.json | 0 .../resources/translations/nested_dict_json/en.json | 0 .../resources/translations/nested_dict_json/pl.json | 0 i18n/tests/loader_tests.py => test/test_loader.py | 0 .../translation_tests.py => test/test_translation.py | 0 17 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 i18n/tests/__init__.py delete mode 100644 i18n/tests/run_tests.py rename {i18n/tests => test}/resources/settings/dummy_config.json (100%) rename {i18n/tests => test}/resources/settings/dummy_config.py (100%) rename {i18n/tests => test}/resources/settings/dummy_config.yml (100%) rename {i18n/tests => test}/resources/settings/eucjp_config.json (100%) rename {i18n/tests => test}/resources/translations/bar/baz.en.json (100%) rename {i18n/tests => test}/resources/translations/en.json (100%) rename {i18n/tests => test}/resources/translations/foo.en.yml (100%) rename {i18n/tests => test}/resources/translations/foo.ja.yml (100%) rename {i18n/tests => test}/resources/translations/gb.json (100%) rename {i18n/tests => test}/resources/translations/ja.json (100%) rename {i18n/tests => test}/resources/translations/nested_dict_json/en.json (100%) rename {i18n/tests => test}/resources/translations/nested_dict_json/pl.json (100%) rename i18n/tests/loader_tests.py => test/test_loader.py (100%) rename i18n/tests/translation_tests.py => test/test_translation.py (100%) diff --git a/.travis.yml b/.travis.yml index 33ee43a..34beff0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,11 @@ 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" # command to run tests diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py deleted file mode 100644 index 5851165..0000000 --- a/i18n/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import translation_tests # noqa: F401 diff --git a/i18n/tests/run_tests.py b/i18n/tests/run_tests.py deleted file mode 100644 index e1ff2d9..0000000 --- a/i18n/tests/run_tests.py +++ /dev/null @@ -1 +0,0 @@ -# 此文件已废弃,迁移到 pytest 后无需此入口。 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/i18n/tests/loader_tests.py b/test/test_loader.py similarity index 100% rename from i18n/tests/loader_tests.py rename to test/test_loader.py diff --git a/i18n/tests/translation_tests.py b/test/test_translation.py similarity index 100% rename from i18n/tests/translation_tests.py rename to test/test_translation.py From 24378a842d521a72d316a155e17bfcd19fc42c5f Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 04:42:41 +0800 Subject: [PATCH 05/20] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DTravis=20CI?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=AD=A5=E9=AA=A4=E5=92=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?pyproject.toml=E4=B8=AD=E7=9A=84=E6=B5=8B=E8=AF=95=E5=8C=85?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 4 +++- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 34beff0..47359d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,9 @@ python: - "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: - pytest --maxfail=1 --disable-warnings --tb=short diff --git a/pyproject.toml b/pyproject.toml index dc55a21..6d6e73d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,4 +31,4 @@ yaml = ["pyyaml>=3.10"] [tool.setuptools.packages.find] where = ["."] -include = ["i18n*", "i18n.loaders*", "i18n.tests*"] +include = ["i18n*", "i18n.loaders*", "test*"] From 6ca161ffe88de659e4e1b353fa3b3c785578fea1 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 04:50:40 +0800 Subject: [PATCH 06/20] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4setup.py?= =?UTF-8?q?=E5=92=8C=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E6=B3=A8=E9=87=8A=E4=BB=A5=E6=B8=85?= =?UTF-8?q?=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 1 - test/test_loader.py | 19 ------------------- test/test_translation.py | 1 - 3 files changed, 21 deletions(-) diff --git a/setup.py b/setup.py index 8a85ba5..eadeef6 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ packages=["i18n", "i18n.loaders", "i18n.tests"], include_package_data=True, zip_safe=True, - # test_suite removed for pytest migration extras_require={ "YAML": ["pyyaml>=3.10"], }, diff --git a/test/test_loader.py b/test/test_loader.py index f81f221..73c4fda 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -1,17 +1,3 @@ -""" -loader_tests.py ----------------- -本文件包含对 i18n.loaders 目录下各类资源加载器的单元测试。 -主要测试 JSON、YAML、Python 格式的配置和翻译文件的加载功能, -确保各类 loader 能正确解析和返回预期的数据结构。 - -测试覆盖点: -- JSONLoader、YAMLLoader、PythonLoader 的基本功能 -- 各类配置文件的加载与异常处理 -- 边界情况与错误输入的健壮性 - -依赖:pytest -""" # -*- encoding: utf-8 -*- from __future__ import unicode_literals @@ -22,7 +8,6 @@ import unittest import pytest -# Python 3 only: always import reload from importlib from importlib import reload from i18n import config, resource_loader, translations @@ -126,14 +111,10 @@ def test_memoization_with_file(self): # 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 assert 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 assert t("memoize.key") == "value" @unittest.skipUnless(json_available, "json library not available") diff --git a/test/test_translation.py b/test/test_translation.py index 436fd83..5aca9af 100644 --- a/test/test_translation.py +++ b/test/test_translation.py @@ -6,7 +6,6 @@ import os.path import pytest -# Python 3 only: always import reload from importlib from importlib import reload from i18n import config, resource_loader, translations From d65e5bdf1039ebf21ed6a159db5c6f8d04bd137e Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 04:55:13 +0800 Subject: [PATCH 07/20] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0CI=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=BB=A5=E7=A1=AE=E4=BF=9D=E6=AD=A3=E7=A1=AE=E5=AE=89?= =?UTF-8?q?=E8=A3=85pytest=E5=B9=B6=E8=B0=83=E6=95=B4lint=E5=92=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 457f16f..a1271c6 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 + run: ruff check . - name: Lint with pyright run: pyright . - name: Run tests - run: python -m i18n.tests.run_tests + run: pytest --maxfail=1 --disable-warnings --tb=short From 89541349b54a1d1038e49cf1d3d67b0cd68f2d1d Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Sun, 18 Jan 2026 04:57:52 +0800 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=86=E9=85=8D=E5=92=8C=E6=98=BE=E7=A4=BA=E9=A1=B6?= =?UTF-8?q?=E7=BA=A7=E9=97=AE=E9=A2=98=E7=9A=84GitHub=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-assign.yml | 19 ++++++++++++ .github/workflows/issues-top.yml | 49 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/auto-assign.yml create mode 100644 .github/workflows/issues-top.yml diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..23a258f --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,19 @@ +name: Auto Assign +on: + issues: + types: [opened] + pull_request: + types: [opened] +jobs: + run: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: "Auto-assign issue" + uses: pozil/auto-assign-issue@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + assignees: xinvxueyuan + numOfAssignee: 1 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 From 4f0f02731fd0b4ac745b653c06bcdc8669f7b044 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Mon, 19 Jan 2026 19:49:21 +0800 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0README=E4=BB=A5?= =?UTF-8?q?=E8=AD=A6=E5=91=8A=E9=A1=B9=E7=9B=AE=E8=BF=87=E6=97=B6=E5=B9=B6?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E7=8E=B0=E4=BB=A3=E5=8C=96=EF=BC=9B=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=BF=BD=E7=95=A5=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ test/test_loader.py | 2 +- test/test_translation.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) 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/test/test_loader.py b/test/test_loader.py index 73c4fda..3a5fa30 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -1,5 +1,5 @@ # -*- encoding: utf-8 -*- - +# type: ignore from __future__ import unicode_literals import os diff --git a/test/test_translation.py b/test/test_translation.py index 5aca9af..87c1be8 100644 --- a/test/test_translation.py +++ b/test/test_translation.py @@ -1,5 +1,5 @@ # -*- encoding: utf-8 -*- - +# type: ignore from __future__ import unicode_literals import os From 2ca53a5d6283d528019b05b8c1e679fbcfb6824c Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Mon, 19 Jan 2026 19:57:06 +0800 Subject: [PATCH 10/20] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4pytest=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E4=BB=8Epyproject.toml=E4=B8=AD=E5=B9=B6=E5=B0=86?= =?UTF-8?q?=E5=85=B6=E6=B7=BB=E5=8A=A0=E5=88=B0=E5=BC=80=E5=8F=91=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E7=BB=84=EF=BC=9B=E6=9B=B4=E6=96=B0requirements.txt?= =?UTF-8?q?=E4=BB=A5=E5=8C=85=E5=90=ABpytest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 8 ++++++-- requirements.txt | 1 + setup.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d6e73d..9df4800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ authors = [ license = { file = "LICENSE" } requires-python = ">=3.7" dependencies = [ - "pytest>=7.4.4", "pyyaml>=3.10", ] classifiers = [ @@ -26,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*", "test*"] +include = ["i18n*", "i18n.loaders*", "tests"] 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 eadeef6..851e0a5 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ 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, extras_require={ From a3c6904a07311177d30d69e0c29b46cb46ff30d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B0v=E5=AD=A6=E5=91=98?= Date: Mon, 19 Jan 2026 20:06:44 +0800 Subject: [PATCH 11/20] Update pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9df4800..7b881c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,4 +35,4 @@ yaml = ["pyyaml>=3.10"] [tool.setuptools.packages.find] where = ["."] -include = ["i18n*", "i18n.loaders*", "tests"] +include = ["i18n*", "i18n.loaders*", "test"] From 0bea323a4e9217e9f34f5b965eb3a117169e2490 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Mon, 19 Jan 2026 20:32:46 +0800 Subject: [PATCH 12/20] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=A1=B6=E5=B1=82=E4=B8=BA=20locale=20=E7=9A=84?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=9B=E9=87=8D=E6=9E=84=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E4=BB=A5=E4=BD=BF=E7=94=A8=20pytest=20?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E5=85=A8=E5=B1=80=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/resource_loader.py | 21 +- test/conftest.py | 4 + test/test_loader.py | 497 ++++++++++++++++++++------------------- test/test_translation.py | 5 +- 4 files changed, 283 insertions(+), 244 deletions(-) create mode 100644 test/conftest.py diff --git a/i18n/resource_loader.py b/i18n/resource_loader.py index 0db3f5f..293291a 100644 --- a/i18n/resource_loader.py +++ b/i18n/resource_loader.py @@ -85,13 +85,26 @@ 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") + # 兼容顶层为 locale 的结构 + # 兼容顶层为 locale 的结构 + if isinstance(dic, dict) and len(dic) == 1 and locale in dic: + dic = dic[locale] + # 避免 namespace 重复叠加 + delimiter = config.get("namespace_delimiter") + + def join_ns(ns, k): + if not ns: + return k + # 如果 ns 已以 k 结尾,避免重复 + if ns.endswith(delimiter + k) or ns == k: + return ns + 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/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/test/test_loader.py b/test/test_loader.py index 3a5fa30..9de6968 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- encoding: utf-8 -*- # type: ignore from __future__ import unicode_literals @@ -5,7 +6,6 @@ import os import os.path import tempfile -import unittest import pytest from importlib import reload @@ -18,242 +18,267 @@ RESOURCE_FOLDER = os.path.join(os.path.dirname(__file__), "resources") -class TestFileLoader: - @pytest.fixture(autouse=True) - def setup_method(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 pytest.raises(I18nFileLoadError) as excinfo: - resource_loader.load_resource("foo.bar", "baz") - assert "no loader" in str(excinfo.value) - - def test_register_wrong_loader(self): - class WrongLoader(object): - pass - - with pytest.raises(ValueError): - resource_loader.register_loader(WrongLoader, []) - - def test_register_python_loader(self): - 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(self): - 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(self): - 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) - - @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" - ) - assert "foo" in data - assert data["foo"] == "bar" - - @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" - ) - assert "foo" in data - assert data["foo"] == "bar" +@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") - 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" - ) - assert "foo" in data - assert data["foo"] == "bar" - - @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) - assert t("memoize.key") == "value" - import shutil - - shutil.rmtree(tmp_dir_name) - assert 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" - ) - assert "ほげ" in data - assert 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) - assert 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) - assert 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") - ) - assert translations.has("foo.normal_key") - assert translations.has("foo.parent.nested_key") +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) - @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") - ) - assert translations.has("foo.mail_number") - translated_plural = translations.get("foo.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." - - @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") - assert 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") - assert 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") - assert 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") - assert translations.has("COMMON.VERSION") - assert 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") - assert translations.has("COMMON.VERSION", locale="pl") - assert 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") - assert translations.has("TOP_MENU.TOP_BAR.LOGS") - assert 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")], + +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" ) - 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" + 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(): + """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" + try: + d = tempfile.TemporaryDirectory() + tmp_dir_name = d.name + except AttributeError: + tmp_dir_name = tempfile.mkdtemp() + fd = open("{}/{}".format(tmp_dir_name, memoization_file_name), "w") + fd.write("en:\n key: value") + fd.close() + 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") + resource_loader.search_translation("foo.normal_key") + 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") + + resource_loader.search_translation("bar.baz.qux") + assert translations.has("foo") + + +@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_neting__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_neting__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/test/test_translation.py b/test/test_translation.py index 87c1be8..78e687e 100644 --- a/test/test_translation.py +++ b/test/test_translation.py @@ -16,11 +16,10 @@ class TestTranslationFormat: @pytest.fixture(scope="class", autouse=True) - def setup_class(cls): + 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"} @@ -52,8 +51,6 @@ def test_missing_translation(self): def test_missing_translation_error(self): config.set("error_on_missing_translation", True) - import pytest - with pytest.raises(KeyError): t("foo.inexistent") From 952c6df506261fb938ca435a13c8abbb8120e360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B0v=E5=AD=A6=E5=91=98?= Date: Mon, 19 Jan 2026 21:11:17 +0800 Subject: [PATCH 13/20] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/auto-assign.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 23a258f..912cd88 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -15,5 +15,6 @@ jobs: uses: pozil/auto-assign-issue@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - assignees: xinvxueyuan + # Override with repository variable AUTO_ASSIGN_ASSIGNEES (comma-separated usernames). Defaults to 'xinvxueyuan'. + assignees: ${{ vars.AUTO_ASSIGN_ASSIGNEES || 'xinvxueyuan' }} numOfAssignee: 1 From f70d6c061b6d0d79bd64e722b438a5df0e9a01b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B0v=E5=AD=A6=E5=91=98?= Date: Mon, 19 Jan 2026 21:11:31 +0800 Subject: [PATCH 14/20] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1271c6..3706c78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: pip install .[yaml] pip install ruff pyright pytest - name: Lint with ruff - run: ruff check . + run: ruff check i18n - name: Lint with pyright run: pyright . - name: Run tests From d2da05b2952c7419807b25fa138fc4190e9c565e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B0v=E5=AD=A6=E5=91=98?= Date: Mon, 19 Jan 2026 21:13:10 +0800 Subject: [PATCH 15/20] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- i18n/resource_loader.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/resource_loader.py b/i18n/resource_loader.py index 293291a..da6cd60 100644 --- a/i18n/resource_loader.py +++ b/i18n/resource_loader.py @@ -86,7 +86,7 @@ def load_translation_file(filename, base_directory, locale=config.get("locale")) def load_translation_dic(dic, namespace, locale): # 兼容顶层为 locale 的结构 - # 兼容顶层为 locale 的结构 + if isinstance(dic, dict) and len(dic) == 1 and locale in dic: dic = dic[locale] # 避免 namespace 重复叠加 diff --git a/pyproject.toml b/pyproject.toml index 7b881c3..e81e11d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,4 +35,4 @@ yaml = ["pyyaml>=3.10"] [tool.setuptools.packages.find] where = ["."] -include = ["i18n*", "i18n.loaders*", "test"] +include = ["i18n*", "i18n.loaders*"] From 957605a65490cd5cc47ec1880787dbfb5ee2b387 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Mon, 19 Jan 2026 21:25:08 +0800 Subject: [PATCH 16/20] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E4=BE=8B=E4=B8=AD=E7=9A=84=E6=8B=BC=E5=86=99?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=B9=B6=E6=9B=B4=E6=96=B0=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=86=E9=85=8D=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=87=8D=E6=96=B0=E6=89=93=E5=BC=80=E7=9A=84=E8=AF=B7?= =?UTF-8?q?=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-assign.yml | 27 ++++++++++++--------------- test/test_translation.py | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 912cd88..8797192 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -1,20 +1,17 @@ name: Auto Assign + on: issues: - types: [opened] - pull_request: - types: [opened] + types: [opened, reopened] + pull_request_target: + types: [opened, reopened] + +permissions: + issues: write + pull-requests: write + jobs: - run: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write + assign-author: + runs-on: ubuntu-slim steps: - - name: "Auto-assign issue" - uses: pozil/auto-assign-issue@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # Override with repository variable AUTO_ASSIGN_ASSIGNEES (comma-separated usernames). Defaults to 'xinvxueyuan'. - assignees: ${{ vars.AUTO_ASSIGN_ASSIGNEES || 'xinvxueyuan' }} - numOfAssignee: 1 + - uses: toshimaru/auto-author-assign@v3.0.1 diff --git a/test/test_translation.py b/test/test_translation.py index 78e687e..0717f5e 100644 --- a/test/test_translation.py +++ b/test/test_translation.py @@ -69,7 +69,7 @@ def test_fallback_from_resource(self): def test_basic_placeholder(self): assert t("foo.hi", name="Bob") == "Hello Bob !" - def test_missing_placehoder(self): + def test_missing_placeholder(self): assert t("foo.hi") == "Hello %{name} !" def test_missing_placeholder_error(self): From 91bfb6cba74836c7ea3a78092c129e2e4668f6ed Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Mon, 19 Jan 2026 22:25:11 +0800 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E7=8A=B6=E6=80=81=E7=9A=84=E6=B5=8B=E8=AF=95=E5=A4=B9?= =?UTF-8?q?=E5=85=B7=E4=BB=A5=E7=A1=AE=E4=BF=9D=E6=B5=8B=E8=AF=95=E4=B9=8B?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E9=85=8D=E7=BD=AE=E9=9A=94=E7=A6=BB=EF=BC=9B?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=92=8C=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_loader.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/test/test_loader.py b/test/test_loader.py index 9de6968..4b4326d 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -28,6 +28,24 @@ def global_setup(): 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 + # 只恢复 config 的用户设置部分,避免破坏模块属性 + 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") @@ -98,22 +116,14 @@ def test_load_python_file(): @pytest.mark.skipif(not yaml_available, reason="yaml library not available") def test_memoization_with_file(): - """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" try: d = tempfile.TemporaryDirectory() tmp_dir_name = d.name except AttributeError: tmp_dir_name = tempfile.mkdtemp() - fd = open("{}/{}".format(tmp_dir_name, memoization_file_name), "w") - fd.write("en:\n key: value") - fd.close() + 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" @@ -192,7 +202,10 @@ def test_search_translation_yaml(): config.set("locale", "en") resource_loader.init_yaml_loader() config.set("file_format", "yml") - resource_loader.search_translation("foo.normal_key") + translations.container.clear() + resource_loader.load_translation_file( + "foo.en.yml", os.path.join(RESOURCE_FOLDER, "translations") + ) assert translations.has("normal_key") From dca293b35fc9a63d7ee805eab07a8497fa37ec15 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Mon, 19 Jan 2026 22:54:21 +0800 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=9A=84=20Ubuntu=20=E7=89=88=E6=9C=AC=EF=BC=9B=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=B5=8B=E8=AF=95=E5=A4=B9=E5=85=B7=E4=BB=A5=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E7=8A=B6=E6=80=81=E9=87=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-assign.yml | 2 +- i18n/resource_loader.py | 6 ------ test/test_loader.py | 3 --- test/test_translation.py | 2 -- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 8797192..bd27ccb 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -12,6 +12,6 @@ permissions: jobs: assign-author: - runs-on: ubuntu-slim + runs-on: ubuntu-latest steps: - uses: toshimaru/auto-author-assign@v3.0.1 diff --git a/i18n/resource_loader.py b/i18n/resource_loader.py index da6cd60..dcee44b 100644 --- a/i18n/resource_loader.py +++ b/i18n/resource_loader.py @@ -85,19 +85,13 @@ def load_translation_file(filename, base_directory, locale=config.get("locale")) def load_translation_dic(dic, namespace, locale): - # 兼容顶层为 locale 的结构 - if isinstance(dic, dict) and len(dic) == 1 and locale in dic: dic = dic[locale] - # 避免 namespace 重复叠加 delimiter = config.get("namespace_delimiter") def join_ns(ns, k): if not ns: return k - # 如果 ns 已以 k 结尾,避免重复 - if ns.endswith(delimiter + k) or ns == k: - return ns return ns + delimiter + k for key, value in dic.items(): diff --git a/test/test_loader.py b/test/test_loader.py index 4b4326d..abc3143 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -30,15 +30,12 @@ def global_setup(): @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 - # 只恢复 config 的用户设置部分,避免破坏模块属性 for k in list(config.__dict__.keys()): if k not in old_config: del config.__dict__[k] diff --git a/test/test_translation.py b/test/test_translation.py index 0717f5e..a7460ec 100644 --- a/test/test_translation.py +++ b/test/test_translation.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -# type: ignore from __future__ import unicode_literals import os From 94a886aab3f461ead8c4ec7516db73f8f9a7e492 Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Tue, 20 Jan 2026 00:10:20 +0800 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=BC=E5=BC=8F=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E7=BF=BB=E8=AF=91=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=9B=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E4=BB=A5=E4=BF=AE=E6=AD=A3=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/resource_loader.py | 8 +++++--- i18n/translations.py | 21 ++++++++++++++++++--- i18n/translator.py | 26 +++++++++++++++++--------- test/test_loader.py | 4 ++-- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/i18n/resource_loader.py b/i18n/resource_loader.py index dcee44b..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 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/test/test_loader.py b/test/test_loader.py index abc3143..087fab4 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -229,7 +229,7 @@ def test_search_translation_without_ns(): @pytest.mark.skipif(not json_available, reason="json library not available") -def test_search_translation_without_ns_nested_dict__two_levels_neting__default_locale(): +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( @@ -245,7 +245,7 @@ def test_search_translation_without_ns_nested_dict__two_levels_neting__default_l @pytest.mark.skipif(not json_available, reason="json library not available") -def test_search_translation_without_ns_nested_dict__two_levels_neting__other_locale(): +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") From c8b9ffc1b6a9f8f3eac63ebd43b0e2f758ce43cb Mon Sep 17 00:00:00 2001 From: xinvxueyuan Date: Tue, 20 Jan 2026 00:14:50 +0800 Subject: [PATCH 20/20] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B0=E7=9A=84=E6=96=87=E4=BB=B6=E5=90=8D=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_loader.py b/test/test_loader.py index 087fab4..3798b67 100644 --- a/test/test_loader.py +++ b/test/test_loader.py @@ -212,9 +212,10 @@ def test_search_translation_json(): 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("foo") + assert translations.has("bar.baz.qux") @pytest.mark.skipif(not json_available, reason="json library not available")