Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-setuptools-gettext for
openSUSE:Factory checked in at 2026-05-20 15:24:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-setuptools-gettext (Old)
and /work/SRC/openSUSE:Factory/.python-setuptools-gettext.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-setuptools-gettext"
Wed May 20 15:24:57 2026 rev:7 rq:1354126 version:0.1.18
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-setuptools-gettext/python-setuptools-gettext.changes
2025-11-10 19:19:41.374252684 +0100
+++
/work/SRC/openSUSE:Factory/.python-setuptools-gettext.new.1966/python-setuptools-gettext.changes
2026-05-20 15:26:11.054250788 +0200
@@ -1,0 +2,12 @@
+Tue May 19 22:04:26 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.1.18:
+ * Add support for `locale//LC_MESSAGES/.po` layout
+ * Allow configuring the `msgfmt` compiler in `pyproject.toml`
+ * Include `.po` files in sdist and run `build_mo` via `python
+ -m build`
+- update to 0.1.17:
+ * Include .po files in sdist and run build_mo via python -m
+ build
+
+-------------------------------------------------------------------
Old:
----
setuptools_gettext-0.1.16.tar.gz
New:
----
setuptools_gettext-0.1.18.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-setuptools-gettext.spec ++++++
--- /var/tmp/diff_new_pack.0Yhy1I/_old 2026-05-20 15:26:12.154296116 +0200
+++ /var/tmp/diff_new_pack.0Yhy1I/_new 2026-05-20 15:26:12.154296116 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-setuptools-gettext
#
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-setuptools-gettext
-Version: 0.1.16
+Version: 0.1.18
Release: 0
Summary: Setuptools gettext extension plugin
License: GPL-2.0-or-later
++++++ setuptools_gettext-0.1.16.tar.gz -> setuptools_gettext-0.1.18.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/setuptools_gettext-0.1.16/PKG-INFO
new/setuptools_gettext-0.1.18/PKG-INFO
--- old/setuptools_gettext-0.1.16/PKG-INFO 2025-10-29 18:41:59.179513200
+0100
+++ new/setuptools_gettext-0.1.18/PKG-INFO 2026-05-19 14:44:18.065828300
+0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: setuptools-gettext
-Version: 0.1.16
+Version: 0.1.18
Summary: Setuptools gettext extension plugin
Maintainer-email: Breezy Developers <[email protected]>
Project-URL: Homepage, https://github.com/breezy-team/setuptools-gettext
@@ -24,8 +24,8 @@
Requires-Dist: setuptools>=61.0
Requires-Dist: tomli>=1.2.1; python_version < "3.11"
Provides-Extra: dev
-Requires-Dist: ruff==0.14.2; extra == "dev"
-Requires-Dist: mypy==1.18.2; extra == "dev"
+Requires-Dist: ruff==0.15.13; extra == "dev"
+Requires-Dist: mypy==1.19.1; extra == "dev"
Provides-Extra: translate-toolkit
Requires-Dist: translate-toolkit>=3.14.0; extra == "translate-toolkit"
Dynamic: license-file
@@ -40,7 +40,10 @@
## Usage
By default, setuptools_gettext compiles and installs mo files when there is a
-`po` directory present that contains ``.po`` files.
+`po` directory present that contains ``.po`` files. It also supports the
+standard gettext layout with ``locale/*/LC_MESSAGES/*.po`` files. If ``po`` is
+absent and a top-level ``locale`` directory contains standard gettext catalogs,
+that directory is used automatically.
The .mo files are installed adjacent to your package as package data in a
subdirectory called ``locale``.
@@ -56,16 +59,37 @@
source_dir = "po"
# directory in which the generated .mo files are placed when building
build_dir = "breezy/locale"
+# compiler to use: "auto", "msgfmt", or "translate-toolkit"
+compiler = "auto"
```
+For standard gettext layouts, point both directories at the locale tree:
+
+```toml
+[tool.setuptools-gettext]
+source_dir = "breezy/locale"
+build_dir = "breezy/locale"
+```
+
+Flat ``po/de.po`` files compile to
+``<build_dir>/de/LC_MESSAGES/<project-name>.mo`` by default. Standard
+``locale/de/LC_MESSAGES/django.po`` files compile to
+``<build_dir>/de/LC_MESSAGES/django.mo`` by default. Passing
+``--output-base`` overrides the output name for both layouts.
+
## Compilation tool
By default, either ``msgfmt`` or the `translate-toolkit` package is used to
compile the .po files into .mo files - whichever is available.
+Set ``compiler = "msgfmt"`` or ``compiler = "translate-toolkit"`` in
+``[tool.setuptools-gettext]`` to force a compiler from ``pyproject.toml``.
+Use ``compiler = "auto"`` to keep the default automatic detection.
+
The ``--msgfmt`` option can be used to force the use of ``msgfmt``, and the
``--translate-toolkit`` option can be used to force the use of the
-translate-toolkit.
+translate-toolkit. Command line options take precedence over the
+``pyproject.toml`` setting.
At the moment, ``msgfmt`` is preferred. In the future, the translate-toolkit
will become the default.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/setuptools_gettext-0.1.16/README.md
new/setuptools_gettext-0.1.18/README.md
--- old/setuptools_gettext-0.1.16/README.md 2025-10-29 18:41:45.000000000
+0100
+++ new/setuptools_gettext-0.1.18/README.md 2026-05-19 14:34:09.000000000
+0200
@@ -8,7 +8,10 @@
## Usage
By default, setuptools_gettext compiles and installs mo files when there is a
-`po` directory present that contains ``.po`` files.
+`po` directory present that contains ``.po`` files. It also supports the
+standard gettext layout with ``locale/*/LC_MESSAGES/*.po`` files. If ``po`` is
+absent and a top-level ``locale`` directory contains standard gettext catalogs,
+that directory is used automatically.
The .mo files are installed adjacent to your package as package data in a
subdirectory called ``locale``.
@@ -24,16 +27,37 @@
source_dir = "po"
# directory in which the generated .mo files are placed when building
build_dir = "breezy/locale"
+# compiler to use: "auto", "msgfmt", or "translate-toolkit"
+compiler = "auto"
```
+For standard gettext layouts, point both directories at the locale tree:
+
+```toml
+[tool.setuptools-gettext]
+source_dir = "breezy/locale"
+build_dir = "breezy/locale"
+```
+
+Flat ``po/de.po`` files compile to
+``<build_dir>/de/LC_MESSAGES/<project-name>.mo`` by default. Standard
+``locale/de/LC_MESSAGES/django.po`` files compile to
+``<build_dir>/de/LC_MESSAGES/django.mo`` by default. Passing
+``--output-base`` overrides the output name for both layouts.
+
## Compilation tool
By default, either ``msgfmt`` or the `translate-toolkit` package is used to
compile the .po files into .mo files - whichever is available.
+Set ``compiler = "msgfmt"`` or ``compiler = "translate-toolkit"`` in
+``[tool.setuptools-gettext]`` to force a compiler from ``pyproject.toml``.
+Use ``compiler = "auto"`` to keep the default automatic detection.
+
The ``--msgfmt`` option can be used to force the use of ``msgfmt``, and the
``--translate-toolkit`` option can be used to force the use of the
-translate-toolkit.
+translate-toolkit. Command line options take precedence over the
+``pyproject.toml`` setting.
At the moment, ``msgfmt`` is preferred. In the future, the translate-toolkit
will become the default.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/setuptools_gettext-0.1.16/pyproject.toml
new/setuptools_gettext-0.1.18/pyproject.toml
--- old/setuptools_gettext-0.1.16/pyproject.toml 2025-10-29
18:41:45.000000000 +0100
+++ new/setuptools_gettext-0.1.18/pyproject.toml 2026-05-19
14:34:09.000000000 +0200
@@ -45,10 +45,13 @@
[project.entry-points."setuptools.finalize_distribution_options"]
setuptools_gettext = "setuptools_gettext:pyprojecttoml_config"
+[project.entry-points."setuptools.file_finders"]
+setuptools_gettext = "setuptools_gettext:find_source_files"
+
[project.optional-dependencies]
dev = [
- "ruff==0.14.2",
- "mypy==1.18.2"
+ "ruff==0.15.13",
+ "mypy==1.19.1"
]
translate-toolkit = [
"translate-toolkit>=3.14.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/setuptools_gettext/__init__.py
new/setuptools_gettext-0.1.18/setuptools_gettext/__init__.py
--- old/setuptools_gettext-0.1.16/setuptools_gettext/__init__.py
2025-10-29 18:41:45.000000000 +0100
+++ new/setuptools_gettext-0.1.18/setuptools_gettext/__init__.py
2026-05-19 14:34:09.000000000 +0200
@@ -22,18 +22,29 @@
import logging
import os
-import re
import sys
-from typing import List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple
from setuptools import Command
from setuptools.dist import Distribution
+from setuptools.errors import OptionError
from setuptools.modified import newer
-__version__ = (0, 1, 16)
+from .catalog import (
+ LC_MESSAGES,
+ Catalog,
+ discover_catalogs,
+ has_standard_catalogs,
+ mo_basename,
+ parse_lang,
+)
+
+__version__ = (0, 1, 18)
DEFAULT_SOURCE_DIR = "po"
DEFAULT_BUILD_DIR = "locale"
DEFAULT_LANGUAGE = "en"
+DEFAULT_COMPILER = "auto"
+VALID_COMPILERS = ("auto", "msgfmt", "translate-toolkit")
def has_translate_toolkit() -> bool:
@@ -48,18 +59,27 @@
return find_executable("msgfmt") is not None
-def lang_from_dir(source_dir: os.PathLike) -> List[str]:
- re_po = re.compile(r"^([a-zA-Z_]+)\.po$")
- lang = []
- for i in os.listdir(source_dir):
- mo = re_po.match(i)
- if mo:
- lang.append(mo.group(1))
- return lang
-
-
-def parse_lang(lang: str) -> List[str]:
- return [i.strip() for i in lang.split(",") if i.strip()]
+def _detect_default_source_dir(dirname: str = "") -> str:
+ po_dir = os.path.join(dirname, DEFAULT_SOURCE_DIR)
+ if os.path.isdir(po_dir):
+ return DEFAULT_SOURCE_DIR
+
+ locale_dir = os.path.join(dirname, DEFAULT_BUILD_DIR)
+ if os.path.isdir(locale_dir) and has_standard_catalogs(locale_dir):
+ return DEFAULT_BUILD_DIR
+
+ return DEFAULT_SOURCE_DIR
+
+
+def _resolve_source_dir(dist: Distribution) -> str:
+ if getattr(dist, "gettext_source_dir_configured", False):
+ return dist.gettext_source_dir # type: ignore
+
+ source_dir = dist.gettext_source_dir # type: ignore
+ if source_dir == DEFAULT_SOURCE_DIR and not os.path.isdir(source_dir):
+ source_dir = _detect_default_source_dir()
+ dist.gettext_source_dir = source_dir # type: ignore
+ return source_dir
# Imported from distutils.util in Python 3.11:
@@ -108,45 +128,78 @@
("lang=", None, "Comma-separated list of languages to process"),
]
- boolean_options = ["force"]
+ boolean_options = ["force", "translate-toolkit", "msgfmt"]
def initialize_options(self):
self.build_dir = None
self.output_base = None
+ self.output_base_explicit = False
self.force = None
self.msgfmt = None
self.translate_toolkit = None
self.lang = None
+ self.catalogs = []
self.outfiles = []
def finalize_options(self):
self.set_undefined_options("build", ("force", "force"))
self.prj_name = self.distribution.get_name()
+ self.output_base_explicit = bool(self.output_base)
if not self.output_base:
self.output_base = self.prj_name or "messages"
- self.source_dir = self.distribution.gettext_source_dir # type: ignore
+ self.source_dir = _resolve_source_dir(self.distribution)
if self.build_dir is None:
self.build_dir = (
getattr(self.distribution, "gettext_build_dir", None)
or DEFAULT_BUILD_DIR
)
+ if self.msgfmt is None and self.translate_toolkit is None:
+ compiler = getattr(
+ self.distribution, "gettext_compiler", DEFAULT_COMPILER
+ )
+ if compiler == "msgfmt":
+ self.msgfmt = True
+ elif compiler == "translate-toolkit":
+ self.translate_toolkit = True
if self.lang is None:
- self.lang = lang_from_dir(self.source_dir)
+ self.catalogs = discover_catalogs(self.source_dir)
else:
- self.lang = parse_lang(self.lang)
+ self.catalogs = discover_catalogs(
+ self.source_dir, parse_lang(self.lang)
+ )
+ self.lang = sorted({catalog.lang for catalog in self.catalogs})
+ self._check_duplicate_outputs()
def get_inputs(self):
- inputs = []
- for lang in self.lang:
- po = os.path.join(self.source_dir, lang + ".po")
- if not os.path.isfile(po):
- po = os.path.join(self.source_dir, lang + ".po")
- inputs.append(po)
- return inputs
+ return [catalog.po for catalog in self.catalogs]
+
+ def _mo_path(self, catalog: Catalog) -> str:
+ domain = catalog.domain
+ if catalog.uses_output_base or self.output_base_explicit:
+ assert self.output_base is not None
+ domain = self.output_base
+ assert self.build_dir is not None
+ return os.path.join(
+ self.build_dir,
+ catalog.lang,
+ LC_MESSAGES,
+ mo_basename(domain),
+ )
+
+ def _check_duplicate_outputs(self) -> None:
+ targets: Dict[str, str] = {}
+ for catalog in self.catalogs:
+ mo = self._mo_path(catalog)
+ if mo in targets:
+ raise OptionError(
+ "Multiple gettext catalogs would compile to "
+ f"{mo}: {targets[mo]} and {catalog.po}"
+ )
+ targets[mo] = catalog.po
def run(self):
"""Run msgfmt for each language."""
- if not self.lang:
+ if not self.catalogs:
return
if self.msgfmt and self.translate_toolkit:
@@ -173,7 +226,10 @@
default_lang = self.distribution.gettext_default_language
- if default_lang in self.lang:
+ if any(
+ catalog.lang == default_lang and catalog.uses_output_base
+ for catalog in self.catalogs
+ ):
if find_executable("msginit") is None:
logging.warning("GNU gettext msginit utility not found!")
logging.warning("Skip creating English PO file.")
@@ -194,20 +250,13 @@
]
)
- basename = self.output_base
- if not basename.endswith(".mo"):
- basename += ".mo"
-
- for lang in self.lang:
- po = os.path.join(self.source_dir, lang + ".po")
- if not os.path.isfile(po):
- po = os.path.join(self.source_dir, lang + ".po")
- dir_ = os.path.join(self.build_dir, lang, "LC_MESSAGES")
+ for catalog in self.catalogs:
+ dir_ = os.path.join(self.build_dir, catalog.lang, LC_MESSAGES)
self.mkpath(dir_)
- mo = os.path.join(dir_, basename)
- if self.force or newer(po, mo):
- logging.info(f"Compile: {po} -> {mo}")
- self.compile_mo(po, mo)
+ mo = self._mo_path(catalog)
+ if self.force or newer(catalog.po, mo):
+ logging.info(f"Compile: {catalog.po} -> {mo}")
+ self.compile_mo(catalog.po, mo)
self.outfiles.append(mo)
def compile_mo(self, po: str, mo: str):
@@ -215,6 +264,7 @@
self.spawn(["msgfmt", "-o", mo, po])
elif self.translate_toolkit:
from translate.tools.pocompile import convertmo
+
with open(po, "rb") as pofile, open(mo, "wb") as mofile:
convertmo(pofile, mofile, None)
else:
@@ -379,7 +429,20 @@
def has_gettext(command) -> bool:
- return os.path.isdir(command.distribution.gettext_source_dir)
+ source_dir = _resolve_source_dir(command.distribution)
+ return os.path.isdir(source_dir)
+
+
+def _load_pyproject_toml(path: str = "pyproject.toml") -> dict:
+ if sys.version_info[:2] >= (3, 11):
+ from tomllib import load as toml_load
+ else:
+ from tomli import load as toml_load
+ try:
+ with open(path, "rb") as f:
+ return toml_load(f).get("tool", {}).get("setuptools-gettext") or {}
+ except FileNotFoundError:
+ return {}
def pyprojecttoml_config(dist: Distribution) -> None:
@@ -390,25 +453,13 @@
install = dist.get_command_class("install")
install.sub_commands.append(("install_mo", has_gettext))
- if sys.version_info[:2] >= (3, 11):
- from tomllib import load as toml_load
- else:
- from tomli import load as toml_load
- try:
- with open("pyproject.toml", "rb") as f:
- cfg = toml_load(f).get("tool", {}).get("setuptools-gettext")
- except FileNotFoundError:
- load_pyproject_config(dist, {})
- else:
- if cfg:
- load_pyproject_config(dist, cfg)
- else:
- load_pyproject_config(dist, {})
+ load_pyproject_config(dist, _load_pyproject_toml())
def load_pyproject_config(dist: Distribution, cfg) -> None:
+ dist.gettext_source_dir_configured = bool(cfg.get("source_dir")) # type:
ignore
dist.gettext_source_dir = ( # type: ignore
- cfg.get("source_dir") or DEFAULT_SOURCE_DIR
+ cfg.get("source_dir") or _detect_default_source_dir()
)
dist.gettext_build_dir = ( # type: ignore
cfg.get("build_dir") or DEFAULT_BUILD_DIR
@@ -416,6 +467,54 @@
dist.gettext_default_language = ( # type: ignore
cfg.get("default_language") or DEFAULT_LANGUAGE
)
+ dist.gettext_compiler = _normalize_compiler( # type: ignore
+ cfg.get("compiler", DEFAULT_COMPILER)
+ )
+
+
+def _normalize_compiler(compiler) -> str:
+ if compiler is None:
+ return DEFAULT_COMPILER
+ if not isinstance(compiler, str):
+ raise ValueError(
+ "Unsupported setuptools-gettext compiler "
+ f"{compiler!r}; expected one of: {', '.join(VALID_COMPILERS)}"
+ )
+ compiler = compiler.strip().lower()
+ if compiler not in VALID_COMPILERS:
+ raise ValueError(
+ "Unsupported setuptools-gettext compiler "
+ f"{compiler!r}; expected one of: {', '.join(VALID_COMPILERS)}"
+ )
+ return compiler
+
+
+def find_source_files(dirname: str = "") -> List[str]:
+ """Find .po/.pot source files for inclusion in the sdist.
+
+ Registered as a ``setuptools.file_finders`` entry point so that
+ ``python -m build --sdist`` ships the gettext source files.
+ """
+ pyproject = (
+ os.path.join(dirname, "pyproject.toml")
+ if dirname
+ else "pyproject.toml"
+ )
+ cfg = _load_pyproject_toml(pyproject)
+ source_dir = cfg.get("source_dir") or _detect_default_source_dir(dirname)
+
+ source_dir_path = (
+ os.path.join(dirname, source_dir) if dirname else source_dir
+ )
+ if not os.path.isdir(source_dir_path):
+ return []
+
+ found = []
+ for root, _dirs, files in os.walk(source_dir_path):
+ for name in files:
+ if name.endswith((".po", ".pot")):
+ found.append(os.path.join(root, name))
+ return found
def find_executable(executable):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/setuptools_gettext/catalog.py
new/setuptools_gettext-0.1.18/setuptools_gettext/catalog.py
--- old/setuptools_gettext-0.1.16/setuptools_gettext/catalog.py 1970-01-01
01:00:00.000000000 +0100
+++ new/setuptools_gettext-0.1.18/setuptools_gettext/catalog.py 2026-05-19
14:34:09.000000000 +0200
@@ -0,0 +1,136 @@
+#
+# Copyright (C) 2007, 2009, 2011 Canonical Ltd.
+# Copyright (C) 2022-2023 Jelmer Vernooij <[email protected]>
+# Copyright (C) 2026 Michal Čihař <[email protected]>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Gettext catalog discovery helpers."""
+
+import os
+from dataclasses import dataclass
+from glob import glob
+from typing import List, Optional
+
+LC_MESSAGES = "LC_MESSAGES"
+
+
+@dataclass(frozen=True)
+class Catalog:
+ lang: str
+ domain: str
+ po: str
+ uses_output_base: bool
+
+
+def lang_from_dir(source_dir: os.PathLike) -> List[str]:
+ lang: List[str] = []
+ if not os.path.isdir(source_dir):
+ return lang
+ for i in os.listdir(source_dir):
+ if i.endswith(".po") and not i.startswith("."):
+ lang.append(i[:-3])
+ return lang
+
+
+def parse_lang(lang: str) -> List[str]:
+ return [i.strip() for i in lang.split(",") if i.strip()]
+
+
+def _flat_catalogs_from_dir(source_dir: os.PathLike) -> List[Catalog]:
+ return [
+ Catalog(
+ lang=lang,
+ domain=lang,
+ po=os.path.join(source_dir, f"{lang}.po"),
+ uses_output_base=True,
+ )
+ for lang in lang_from_dir(source_dir)
+ ]
+
+
+def _standard_catalogs_from_dir(source_dir: os.PathLike) -> List[Catalog]:
+ catalogs = []
+ pattern = os.path.join(source_dir, "*", LC_MESSAGES, "*.po")
+ for po in glob(pattern):
+ lang = os.path.basename(os.path.dirname(os.path.dirname(po)))
+ domain = os.path.splitext(os.path.basename(po))[0]
+ catalogs.append(
+ Catalog(
+ lang=lang,
+ domain=domain,
+ po=po,
+ uses_output_base=False,
+ )
+ )
+ return catalogs
+
+
+def discover_catalogs(
+ source_dir: os.PathLike, lang: Optional[List[str]] = None
+) -> List[Catalog]:
+ if lang is None:
+ catalogs = _flat_catalogs_from_dir(source_dir)
+ catalogs.extend(_standard_catalogs_from_dir(source_dir))
+ return sorted(catalogs, key=lambda catalog: catalog.po)
+
+ catalogs = []
+ for language in lang:
+ found = False
+ flat_po = os.path.join(source_dir, f"{language}.po")
+ if os.path.isfile(flat_po):
+ found = True
+ catalogs.append(
+ Catalog(
+ lang=language,
+ domain=language,
+ po=flat_po,
+ uses_output_base=True,
+ )
+ )
+
+ pattern = os.path.join(source_dir, language, LC_MESSAGES, "*.po")
+ for po in sorted(glob(pattern)):
+ found = True
+ catalogs.append(
+ Catalog(
+ lang=language,
+ domain=os.path.splitext(os.path.basename(po))[0],
+ po=po,
+ uses_output_base=False,
+ )
+ )
+
+ if not found:
+ catalogs.append(
+ Catalog(
+ lang=language,
+ domain=language,
+ po=flat_po,
+ uses_output_base=True,
+ )
+ )
+ return catalogs
+
+
+def has_standard_catalogs(source_dir: str) -> bool:
+ pattern = os.path.join(source_dir, "*", LC_MESSAGES, "*.po")
+ return bool(glob(pattern))
+
+
+def mo_basename(name: str) -> str:
+ if name.endswith(".mo"):
+ return name
+ return f"{name}.mo"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/PKG-INFO
new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/PKG-INFO
--- old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/PKG-INFO
2025-10-29 18:41:59.000000000 +0100
+++ new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/PKG-INFO
2026-05-19 14:44:18.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: setuptools-gettext
-Version: 0.1.16
+Version: 0.1.18
Summary: Setuptools gettext extension plugin
Maintainer-email: Breezy Developers <[email protected]>
Project-URL: Homepage, https://github.com/breezy-team/setuptools-gettext
@@ -24,8 +24,8 @@
Requires-Dist: setuptools>=61.0
Requires-Dist: tomli>=1.2.1; python_version < "3.11"
Provides-Extra: dev
-Requires-Dist: ruff==0.14.2; extra == "dev"
-Requires-Dist: mypy==1.18.2; extra == "dev"
+Requires-Dist: ruff==0.15.13; extra == "dev"
+Requires-Dist: mypy==1.19.1; extra == "dev"
Provides-Extra: translate-toolkit
Requires-Dist: translate-toolkit>=3.14.0; extra == "translate-toolkit"
Dynamic: license-file
@@ -40,7 +40,10 @@
## Usage
By default, setuptools_gettext compiles and installs mo files when there is a
-`po` directory present that contains ``.po`` files.
+`po` directory present that contains ``.po`` files. It also supports the
+standard gettext layout with ``locale/*/LC_MESSAGES/*.po`` files. If ``po`` is
+absent and a top-level ``locale`` directory contains standard gettext catalogs,
+that directory is used automatically.
The .mo files are installed adjacent to your package as package data in a
subdirectory called ``locale``.
@@ -56,16 +59,37 @@
source_dir = "po"
# directory in which the generated .mo files are placed when building
build_dir = "breezy/locale"
+# compiler to use: "auto", "msgfmt", or "translate-toolkit"
+compiler = "auto"
```
+For standard gettext layouts, point both directories at the locale tree:
+
+```toml
+[tool.setuptools-gettext]
+source_dir = "breezy/locale"
+build_dir = "breezy/locale"
+```
+
+Flat ``po/de.po`` files compile to
+``<build_dir>/de/LC_MESSAGES/<project-name>.mo`` by default. Standard
+``locale/de/LC_MESSAGES/django.po`` files compile to
+``<build_dir>/de/LC_MESSAGES/django.mo`` by default. Passing
+``--output-base`` overrides the output name for both layouts.
+
## Compilation tool
By default, either ``msgfmt`` or the `translate-toolkit` package is used to
compile the .po files into .mo files - whichever is available.
+Set ``compiler = "msgfmt"`` or ``compiler = "translate-toolkit"`` in
+``[tool.setuptools-gettext]`` to force a compiler from ``pyproject.toml``.
+Use ``compiler = "auto"`` to keep the default automatic detection.
+
The ``--msgfmt`` option can be used to force the use of ``msgfmt``, and the
``--translate-toolkit`` option can be used to force the use of the
-translate-toolkit.
+translate-toolkit. Command line options take precedence over the
+``pyproject.toml`` setting.
At the moment, ``msgfmt`` is preferred. In the future, the translate-toolkit
will become the default.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/SOURCES.txt
new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/SOURCES.txt
--- old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/SOURCES.txt
2025-10-29 18:41:59.000000000 +0100
+++ new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/SOURCES.txt
2026-05-19 14:44:18.000000000 +0200
@@ -12,6 +12,7 @@
example/po/hallowereld.pot
example/po/nl.po
setuptools_gettext/__init__.py
+setuptools_gettext/catalog.py
setuptools_gettext.egg-info/PKG-INFO
setuptools_gettext.egg-info/SOURCES.txt
setuptools_gettext.egg-info/dependency_links.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/entry_points.txt
new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/entry_points.txt
--- old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/entry_points.txt
2025-10-29 18:41:59.000000000 +0100
+++ new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/entry_points.txt
2026-05-19 14:44:18.000000000 +0200
@@ -4,5 +4,8 @@
install_mo = setuptools_gettext:install_mo
update_pot = setuptools_gettext:update_pot
+[setuptools.file_finders]
+setuptools_gettext = setuptools_gettext:find_source_files
+
[setuptools.finalize_distribution_options]
setuptools_gettext = setuptools_gettext:pyprojecttoml_config
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/requires.txt
new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/requires.txt
--- old/setuptools_gettext-0.1.16/setuptools_gettext.egg-info/requires.txt
2025-10-29 18:41:59.000000000 +0100
+++ new/setuptools_gettext-0.1.18/setuptools_gettext.egg-info/requires.txt
2026-05-19 14:44:18.000000000 +0200
@@ -4,8 +4,8 @@
tomli>=1.2.1
[dev]
-ruff==0.14.2
-mypy==1.18.2
+ruff==0.15.13
+mypy==1.19.1
[translate-toolkit]
translate-toolkit>=3.14.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/setuptools_gettext-0.1.16/tests/test_example.py
new/setuptools_gettext-0.1.18/tests/test_example.py
--- old/setuptools_gettext-0.1.16/tests/test_example.py 2025-10-29
18:41:45.000000000 +0100
+++ new/setuptools_gettext-0.1.18/tests/test_example.py 2026-05-19
14:34:09.000000000 +0200
@@ -1,12 +1,13 @@
import os
import shutil
from tempfile import TemporaryDirectory
-from unittest import SkipTest
+import pytest
from setuptools import Distribution
from setuptools_gettext import (
build_mo,
+ find_source_files,
install_mo,
load_pyproject_config,
update_pot,
@@ -54,10 +55,115 @@
os.chdir(old_cwd)
+def test_load_pyproject_config_default_compiler():
+ dist = Distribution()
+
+ load_pyproject_config(dist, {})
+
+ assert getattr(dist, "gettext_compiler") == "auto"
+
+
[email protected]("compiler", ["auto", "msgfmt", "translate-toolkit"])
+def test_load_pyproject_config_compiler(compiler):
+ dist = Distribution()
+
+ load_pyproject_config(dist, {"compiler": compiler})
+
+ assert getattr(dist, "gettext_compiler") == compiler
+
+
+def test_load_pyproject_config_rejects_invalid_compiler():
+ dist = Distribution()
+
+ with pytest.raises(ValueError, match="Unsupported setuptools-gettext"):
+ load_pyproject_config(dist, {"compiler": "invalid"})
+
+
+def test_build_mo_uses_msgfmt_compiler_from_config():
+ with TemporaryDirectory() as td:
+ source_dir = os.path.join(td, "po")
+ os.mkdir(source_dir)
+ with open(os.path.join(source_dir, "nl.po"), "w") as f:
+ f.write("")
+ dist = Distribution(attrs={"name": "example"})
+
+ load_pyproject_config(
+ dist, {"source_dir": source_dir, "compiler": "msgfmt"}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.finalize_options()
+
+ assert cmd.msgfmt is True
+ assert cmd.translate_toolkit is None
+
+
+def test_build_mo_uses_translate_toolkit_compiler_from_config():
+ with TemporaryDirectory() as td:
+ source_dir = os.path.join(td, "po")
+ os.mkdir(source_dir)
+ with open(os.path.join(source_dir, "nl.po"), "w") as f:
+ f.write("")
+ dist = Distribution(attrs={"name": "example"})
+
+ load_pyproject_config(
+ dist, {"source_dir": source_dir, "compiler": "translate-toolkit"}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.finalize_options()
+
+ assert cmd.msgfmt is None
+ assert cmd.translate_toolkit is True
+
+
[email protected](
+ ("compiler", "flag", "expected_msgfmt", "expected_translate_toolkit"),
+ [
+ ("translate-toolkit", "msgfmt", True, None),
+ ("msgfmt", "translate_toolkit", None, True),
+ ],
+)
+def test_build_mo_cli_compiler_flags_override_config(
+ compiler, flag, expected_msgfmt, expected_translate_toolkit
+):
+ with TemporaryDirectory() as td:
+ source_dir = os.path.join(td, "po")
+ os.mkdir(source_dir)
+ with open(os.path.join(source_dir, "nl.po"), "w") as f:
+ f.write("")
+ dist = Distribution(attrs={"name": "example"})
+
+ load_pyproject_config(
+ dist, {"source_dir": source_dir, "compiler": compiler}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ setattr(cmd, flag, True)
+ cmd.finalize_options()
+
+ assert cmd.msgfmt is expected_msgfmt
+ assert cmd.translate_toolkit is expected_translate_toolkit
+
+
+def test_find_source_files_example():
+ found = find_source_files("example")
+ rel = sorted(os.path.relpath(p, "example") for p in found)
+ assert rel == [
+ os.path.join("po", "hallowereld.pot"),
+ os.path.join("po", "nl.po"),
+ ]
+
+
+def test_find_source_files_missing_dir():
+ with TemporaryDirectory() as td:
+ assert find_source_files(td) == []
+
+
def test_update_pot():
# Skip this test if xgettext is not available
if shutil.which("xgettext") is None:
- raise SkipTest("xgettext not available")
+ pytest.skip("xgettext not available")
with TemporaryDirectory() as td:
shutil.copytree("example", td + "/example")
p = os.path.join(td, "example", "hallowereld", "example.py")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/setuptools_gettext-0.1.16/tests/test_setuptools_gettext.py
new/setuptools_gettext-0.1.18/tests/test_setuptools_gettext.py
--- old/setuptools_gettext-0.1.16/tests/test_setuptools_gettext.py
2025-10-29 18:41:45.000000000 +0100
+++ new/setuptools_gettext-0.1.18/tests/test_setuptools_gettext.py
2026-05-19 14:34:09.000000000 +0200
@@ -1,7 +1,26 @@
import os
from tempfile import TemporaryDirectory
-from setuptools_gettext import gather_built_files, lang_from_dir, parse_lang
+import pytest
+from setuptools import Distribution
+from setuptools.errors import OptionError
+
+import setuptools_gettext
+from setuptools_gettext import (
+ build_mo,
+ discover_catalogs,
+ find_source_files,
+ gather_built_files,
+ load_pyproject_config,
+ parse_lang,
+)
+from setuptools_gettext.catalog import lang_from_dir
+
+
+def write_file(path):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w") as f:
+ f.write("foo")
def test_lang_from_dir():
@@ -14,8 +33,10 @@
f.write("foo")
with open(os.path.join(podir, "de_DE.po"), "w") as f:
f.write("foo")
+ with open(os.path.join(podir, "pt-BR.po"), "w") as f:
+ f.write("foo")
- assert set(lang_from_dir(podir)) == {"de", "de_DE", "fr"}
+ assert set(lang_from_dir(podir)) == {"de", "de_DE", "fr", "pt-BR"}
def test_parse_lang():
@@ -39,3 +60,182 @@
os.path.join(de_lc_messages_dir, "app.mo"),
os.path.join(de_lc_messages_dir, "app2.mo"),
}
+
+
+def test_discover_catalogs_standard_layout():
+ with TemporaryDirectory() as td:
+ locale = os.path.join(td, "locale")
+ po = os.path.join(locale, "pt-BR", "LC_MESSAGES", "django.po")
+ write_file(po)
+
+ catalogs = discover_catalogs(locale)
+
+ assert catalogs[0].lang == "pt-BR"
+ assert catalogs[0].domain == "django"
+ assert catalogs[0].po == po
+ assert not catalogs[0].uses_output_base
+
+
+def run_build(cmd, monkeypatch):
+ compiled = []
+
+ def compile_mo(po, mo) -> None:
+ compiled.append((po, mo))
+ write_file(mo)
+
+ monkeypatch.setattr(setuptools_gettext, "has_msgfmt", lambda: True)
+ cmd.compile_mo = compile_mo
+ cmd.run()
+ return compiled
+
+
+def test_build_standard_layout_uses_po_filename_domain(monkeypatch):
+ with TemporaryDirectory() as td:
+ locale = os.path.join(td, "locale")
+ po = os.path.join(locale, "de", "LC_MESSAGES", "django.po")
+ write_file(po)
+ build_dir = os.path.join(td, "build")
+ dist = Distribution(attrs={"name": "demo"})
+ load_pyproject_config(
+ dist, {"source_dir": locale, "build_dir": build_dir}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.finalize_options()
+
+ compiled = run_build(cmd, monkeypatch)
+
+ mo = os.path.join(build_dir, "de", "LC_MESSAGES", "django.mo")
+ assert compiled == [(po, mo)]
+ assert cmd.get_outputs() == [mo]
+
+
+def test_build_standard_layout_multiple_domains(monkeypatch):
+ with TemporaryDirectory() as td:
+ locale = os.path.join(td, "locale")
+ django_po = os.path.join(locale, "de", "LC_MESSAGES", "django.po")
+ djangojs_po = os.path.join(locale, "de", "LC_MESSAGES", "djangojs.po")
+ write_file(django_po)
+ write_file(djangojs_po)
+ build_dir = os.path.join(td, "build")
+ dist = Distribution(attrs={"name": "demo"})
+ load_pyproject_config(
+ dist, {"source_dir": locale, "build_dir": build_dir}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.finalize_options()
+
+ compiled = set(run_build(cmd, monkeypatch))
+
+ assert compiled == {
+ (
+ django_po,
+ os.path.join(build_dir, "de", "LC_MESSAGES", "django.mo"),
+ ),
+ (
+ djangojs_po,
+ os.path.join(build_dir, "de", "LC_MESSAGES", "djangojs.mo"),
+ ),
+ }
+
+
+def test_build_standard_layout_output_base_overrides_domain(monkeypatch):
+ with TemporaryDirectory() as td:
+ locale = os.path.join(td, "locale")
+ po = os.path.join(locale, "de", "LC_MESSAGES", "django.po")
+ write_file(po)
+ build_dir = os.path.join(td, "build")
+ dist = Distribution(attrs={"name": "demo"})
+ load_pyproject_config(
+ dist, {"source_dir": locale, "build_dir": build_dir}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.output_base = "custom"
+ cmd.finalize_options()
+
+ compiled = run_build(cmd, monkeypatch)
+
+ mo = os.path.join(build_dir, "de", "LC_MESSAGES", "custom.mo")
+ assert compiled == [(po, mo)]
+ assert cmd.get_outputs() == [mo]
+
+
+def test_build_mixed_layouts_without_output_collision(monkeypatch):
+ with TemporaryDirectory() as td:
+ source = os.path.join(td, "po")
+ flat_po = os.path.join(source, "de.po")
+ standard_po = os.path.join(source, "de", "LC_MESSAGES", "django.po")
+ write_file(flat_po)
+ write_file(standard_po)
+ build_dir = os.path.join(td, "build")
+ dist = Distribution(attrs={"name": "demo"})
+ load_pyproject_config(
+ dist, {"source_dir": source, "build_dir": build_dir}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.finalize_options()
+
+ compiled = set(run_build(cmd, monkeypatch))
+
+ assert compiled == {
+ (
+ flat_po,
+ os.path.join(build_dir, "de", "LC_MESSAGES", "demo.mo"),
+ ),
+ (
+ standard_po,
+ os.path.join(build_dir, "de", "LC_MESSAGES", "django.mo"),
+ ),
+ }
+
+
+def test_duplicate_output_paths_are_rejected():
+ with TemporaryDirectory() as td:
+ locale = os.path.join(td, "locale")
+ write_file(os.path.join(locale, "de", "LC_MESSAGES", "django.po"))
+ write_file(os.path.join(locale, "de", "LC_MESSAGES", "app.po"))
+ dist = Distribution(attrs={"name": "demo"})
+ build_dir = os.path.join(td, "build")
+ load_pyproject_config(
+ dist, {"source_dir": locale, "build_dir": build_dir}
+ )
+ cmd = build_mo(dist)
+ cmd.initialize_options()
+ cmd.output_base = "custom"
+
+ with pytest.raises(OptionError, match="Multiple gettext catalogs"):
+ cmd.finalize_options()
+
+
+def test_load_pyproject_config_auto_detects_locale_without_po():
+ with TemporaryDirectory() as td:
+ write_file(
+ os.path.join(td, "locale", "de", "LC_MESSAGES", "django.po")
+ )
+ old_cwd = os.getcwd()
+ os.chdir(td)
+ try:
+ dist = Distribution(attrs={"name": "demo"})
+ load_pyproject_config(dist, {})
+ finally:
+ os.chdir(old_cwd)
+
+ assert dist.gettext_source_dir == "locale"
+
+
+def test_find_source_files_auto_detects_locale():
+ with TemporaryDirectory() as td:
+ write_file(os.path.join(td, "locale", "django.pot"))
+ write_file(
+ os.path.join(td, "locale", "de", "LC_MESSAGES", "django.po")
+ )
+
+ found = find_source_files(td)
+
+ assert sorted(os.path.relpath(path, td) for path in found) == [
+ os.path.join("locale", "de", "LC_MESSAGES", "django.po"),
+ os.path.join("locale", "django.pot"),
+ ]