Skip to content

Scaffolding: lint-po rejects msgid_plural — can't use Babel plural forms #1719

@davidpoblador

Description

@davidpoblador

Bug

The shipped `lint-po` recipe (`just lint-po` → `uvx lint-po locales//LC_MESSAGES/.po`) rejects any `.po` file that uses GNU gettext plural forms.

Reproducer

A perfectly valid Babel-extracted plural entry:

```po
#: templates/frontend/directory.html.jinja:22
#, python-format
msgid "One podcast on the air."
msgid_plural "%(count)s podcasts on the air."
msgstr[0] "Un podcast emetent."
msgstr[1] "%(count)s podcasts emetent."
```

Generated by:

```jinja
{% trans count=entries | length %}
One podcast on the air.
{% pluralize %}
{{ count }} podcasts on the air.
{% endtrans %}
```

`lint-po` walks the file line-by-line with a small state machine that knows about `msgid` and `msgstr` but not `msgid_plural`, `msgstr[0]`, `msgstr[1]`, etc. Each unknown line triggers `warnings.warn("(state) Unexpected input: ...")` and bumps an internal `errors = True`. `main` then returns `1`, so `just lint-po` fails. `just lint` (which depends on `lint-po`) fails too.

Why it matters

Plural forms aren't optional polish — they're how GNU gettext expects you to localize "1 X / N X" patterns. Several target locales need the distinction (German, Catalan, Spanish, etc.). Without plural-form support in the linter, projects either:

  • Skip pluralization entirely and write awkward fallbacks like `{% if n == 1 %}…{% else %}…{% endif %}` with two separate `msgid`s. (Workaround we just did in radio — losing N-form variants in languages that need them.)
  • Disable the lint step.
  • Switch to a different linter.

This is a footgun: scaffolded projects start clean, hit it the first time someone uses `{% trans count=… %}{% pluralize %}…{% endtrans %}`, and have to choose between losing pluralization or losing the linter.

Fix options

A. Replace `lint-po` with something plural-aware. Options:

  • `msgfmt --check` (from gettext-tools) — battle-tested, ships with most distros, supports plural forms, but adds a system dependency outside uv/uvx.
  • `polib`-based linter (Python). `polib.pofile().percent_translated()` etc., plus a small wrapper. Stays in the uv toolchain.
  • `dennis-cmd` (https://github.com/willkg/dennis) — plural-aware, Python-based, packaged.

(B is probably the cleanest for vibetuner: stay in the uv ecosystem, support plurals, add it as a vibetuner-shipped CLI command.)

B. Drop the lint-po recipe. PO file syntax is mechanically validated by `pybabel compile` already; `lint-po` adds little beyond that. `just compile-locales` already fails on malformed files. Removing the duplicate check means one fewer scaffolded gotcha.

C. Patch `lint-po` upstream to handle plurals. Doable but the project (https://github.com/zaufi/lint-po) hasn't seen activity in a while. Might or might not get accepted.

We did A-via-workaround in radio (alltuner/radio#410) by avoiding plural forms entirely. Not a great precedent.

Pointers

  • Scaffolded recipe: `.justfiles/linting.justfile` → `lint-po:` target.
  • Failing input source: any Babel `{% trans count=... %}{% pluralize %}{% endtrans %}` block.
  • `lint-po` source: https://github.com/zaufi/lint-po/blob/master/lint_po/main.py (the state machine that doesn't know about `msgid_plural` / `msgstr[N]`).

Filed by Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions