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"),
+        ]

Reply via email to