pockingbird 0.2.0

Report-only CLI that finds duplicate translation keys across gettext .po catalogs
Documentation

pockingbird

CI License: MIT

A CLI that finds duplicate translation keys in gettext .po catalogs.

Over time a catalog grows keys whose translations are identical across every locale — real duplicates that can collapse into one key — plus near-duplicates that differ only by trailing punctuation, a typo, or one disagreeing locale. pockingbird surfaces all of them in a single report.

It is report-only: it never edits your .po files. The decision to collapse keys stays with you.

How it works

  1. Discover every .po file under the target path.
  2. Parse each locale into a matrix key × locale → translation.
  3. Group keys with matching translation signatures, ranked by how many locales agree.

A key's translation vector is its signature; two keys match when their vectors agree. Matching runs at three tiers:

  • exact — byte-equal after trimming.
  • normalized — equal after case-folding, whitespace collapsing, and trailing punctuation stripping (Ok vs OK.).
  • fuzzy ≤2 — within Levenshtein distance 2 (typos, stray punctuation). Per locale, near strings are clustered (BK-tree + union-find) and compared by cluster.

Within each tier, groups are reported by agreement level — full M/M first, then M−1/M, M−2/M, … down to a configurable floor (min_locales_agree). Empty cells and whole incomplete locales are handled via config.

Install

cargo install --path .

Usage

pockingbird find .                              # current dir, text report
pockingbird find ./locales --format json        # JSON for pipelines
pockingbird find . --preset strict              # exact-match only, no config file
pockingbird find . --min-agree 3 --tier exact   # override knobs from the CLI
pockingbird find . --config pockingbird.toml    # with a config file
pockingbird init                                # write a starter pockingbird.toml
  • text (default) — colored sections per tier, each grouped by level. Every group lists its keys, shared translations, and diverging locales; cross-domain groups get a "unify into a shared domain" hint.
  • json — the same structure as machine-readable data.

Exit code is always 0 — it reports, it does not gate. If min_locales_agree exceeds the number of locales found, it is lowered to that count (with a warning) so the report is never silently empty.

Configuration

Run pockingbird init to drop a commented pockingbird.toml that mirrors the built-in defaults. The fastest way to configure matching is a preset; tune individual knobs on top only if you need to:

[match]
preset = "balanced"       # strict | balanced | loose
# any explicit field below overrides the preset baseline:
# min_locales_agree = 5
  • strict — exact tier only, no normalization (byte-identical translations).
  • balanced (default) — exact + normalized + fuzzy≤2, normalization on.
  • loose — balanced with a wider fuzzy radius (≤3) and a lower length floor.

CLI flags (--preset, --min-agree, --tier, --exclude, --format) override the config file, which overrides the preset, which overrides the built-in defaults. Precedence is resolved per field: a CLI --preset re-bases the baseline, but any knob you set explicitly in the file still wins over it. See pockingbird.toml for every knob.

Library

use std::path::Path;
use pockingbird::{scan, Config};

let report = scan(Path::new("./locales"), &Config::default())?;
let json = pockingbird::report::to_json(&report.groups, report.total_keys);

Domain terms are defined in CONTEXT.md. Example catalogs live in fixtures/; an end-to-end test in tests/integration.rs.