Summary
make.py's i18n string extraction (init / po/template.pot generation) only
scans an addon's top-level *.py files. For a nested-package addon — one
that ships a Python package tree (sub-packages under the addon dir) and puts its
translatable strings in those nested modules — the generated template.pot
silently misses every _() string outside the top level.
Background
NameSuite (upstream PR 941) is the first addon submitted as a nested Python
package: a name_processor/ library tree (controllers/, models/,
services/, repositories/, views/, …) plus a parallel tests/ tree, wired
together with absolute imports. The runtime model is valid — Gramps' plugin
loader inserts the addon dir onto sys.path before importing
(gramps/gen/plug/_manager.py:311), so from name_processor… import … resolves.
But its translatable strings live almost entirely in the nested modules. The two
top-level entry files declare none; the .gpr.py has a handful; the rest are in
name_processor/views/*:
| module |
_() strings |
views/tool_audit_tab.py |
21 |
views/tool_rename_tab.py |
20 |
views/gramplet.py |
11 |
views/tool.py |
3 |
views/base_tab.py |
1 |
≈56 UI strings, none of which the current extractor sees.
Problem
In the init command, string extraction globs only the addon's top-level .py:
# make.py (init command)
fnames = glob.glob("%s/*.py" % addon) # ~line 381 (gpr discovery)
...
fnames = " ".join(glob.glob(f"{addon}/*.py")) # ~line 404 (xgettext input)
system(
f"xgettext --language=Python --keyword=_ --keyword=_:1,2c --keyword=N_"
f" --from-code=UTF-8 --add-comments=Translators"
f' -o "{addon}/po/template.pot" {fnames} '
)
{addon}/*.py matches only files directly under the addon dir, not
{addon}/<pkg>/**/*.py. So for a nested-package addon the resulting
template.pot contains only the top-level / .gpr.py strings and silently
omits the nested-module strings. Because the structure check only verifies that
po/template.pot exists, a near-empty template still passes — the gap is
invisible. Translators never receive the missing strings.
Proposed fix
Extract over the addon's full package tree (recursively), excluding tests, e.g.:
import os
py_files = [
os.path.join(root, f)
for root, _dirs, files in os.walk(addon)
for f in files
if f.endswith(".py") and "/tests/" not in os.path.join(root, f).replace(os.sep, "/") + "/"
]
or the shell equivalent already proven to capture the NameSuite strings:
xgettext --language=Python --keyword=_ --keyword=_:1,2c --keyword=N_ \
--from-code=UTF-8 --add-comments=Translators \
-o "<Addon>/po/template.pot" \
$(find <Addon> -name '*.py' -not -path '*/tests/*')
Glade/XML handling stays as-is. Flat addons are unaffected (their *.py is the
whole tree).
Acceptance criteria
make.py <ver> init <NestedAddon> produces a template.pot containing the
_() strings from nested package modules (verify against a nested addon: the
≈56 NameSuite view strings appear).
- Flat addons produce byte-identical templates to today (no spurious churn).
- Test files are excluded from extraction.
Related / minor
The default build globs ({addon}/*.py) plus a typical MANIFEST like
name_processor/*/* don't capture the top-level package marker
name_processor/__init__.py (only nested files, which do_tar ships correctly
via recursive tar.add). It imports fine anyway as a PEP 420 namespace package,
so this is cosmetic — but a recursive build/manifest helper could ship the
explicit marker too. Out of scope for the extraction fix; noting it as the same
"make.py assumes a flat addon" theme.
Summary
make.py's i18n string extraction (init/po/template.potgeneration) onlyscans an addon's top-level
*.pyfiles. For a nested-package addon — onethat ships a Python package tree (sub-packages under the addon dir) and puts its
translatable strings in those nested modules — the generated
template.potsilently misses every
_()string outside the top level.Background
NameSuite (upstream PR 941) is the first addon submitted as a nested Python
package: a
name_processor/library tree (controllers/,models/,services/,repositories/,views/, …) plus a paralleltests/tree, wiredtogether with absolute imports. The runtime model is valid — Gramps' plugin
loader inserts the addon dir onto
sys.pathbefore importing(
gramps/gen/plug/_manager.py:311), sofrom name_processor… import …resolves.But its translatable strings live almost entirely in the nested modules. The two
top-level entry files declare none; the
.gpr.pyhas a handful; the rest are inname_processor/views/*:_()stringsviews/tool_audit_tab.pyviews/tool_rename_tab.pyviews/gramplet.pyviews/tool.pyviews/base_tab.py≈56 UI strings, none of which the current extractor sees.
Problem
In the
initcommand, string extraction globs only the addon's top-level.py:{addon}/*.pymatches only files directly under the addon dir, not{addon}/<pkg>/**/*.py. So for a nested-package addon the resultingtemplate.potcontains only the top-level /.gpr.pystrings and silentlyomits the nested-module strings. Because the structure check only verifies that
po/template.potexists, a near-empty template still passes — the gap isinvisible. Translators never receive the missing strings.
Proposed fix
Extract over the addon's full package tree (recursively), excluding tests, e.g.:
or the shell equivalent already proven to capture the NameSuite strings:
Glade/XML handling stays as-is. Flat addons are unaffected (their
*.pyis thewhole tree).
Acceptance criteria
make.py <ver> init <NestedAddon>produces atemplate.potcontaining the_()strings from nested package modules (verify against a nested addon: the≈56 NameSuite view strings appear).
Related / minor
The default build globs (
{addon}/*.py) plus a typical MANIFEST likename_processor/*/*don't capture the top-level package markername_processor/__init__.py(only nested files, whichdo_tarships correctlyvia recursive
tar.add). It imports fine anyway as a PEP 420 namespace package,so this is cosmetic — but a recursive build/manifest helper could ship the
explicit marker too. Out of scope for the extraction fix; noting it as the same
"make.py assumes a flat addon" theme.