npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

po-genie

v0.4.1

Published

AI-powered gettext PO/POT and WordPress readme.txt translator

Readme

po-genie

AI-powered translator for gettext .po / .pot catalogs and WordPress readme.txt files. It uses any model available on OpenRouter, preserves structural fragments, and validates every translated batch before accepting it.

Features

  • Translates missing gettext entries or the translatable parts of a WordPress readme
  • Generates every gettext plural form and sets the target Language and Plural-Forms headers
  • Protects variables, HTML, URLs, Markdown destinations, and inline code from structural changes
  • Validates response counts and protected fragments before applying a batch
  • Resumes interrupted work from safe, identity-checked checkpoints
  • Writes final outputs and checkpoints atomically
  • Runs multiple target locales with configurable bounded concurrency
  • Returns known token usage and optional cost estimates through the API

Quick start

1. Get an API key from openrouter.ai/keys — free to sign up.

2. Run without installing:

# Using npx
OPENROUTER_API_KEY=sk-or-... npx po-genie -i languages/messages.pot -l nl_NL

# Using pnpm
OPENROUTER_API_KEY=sk-or-... pnpm dlx po-genie -i languages/messages.pot -l nl_NL

Or put your key in a .env file in your project root and it will be picked up automatically:

# .env
OPENROUTER_API_KEY=sk-or-...
npx po-genie -i languages/messages.pot -l nl_NL

3. Or install globally:

npm install -g po-genie
# then
po-genie -i languages/messages.pot -l nl_NL

Examples

# Translate a .pot file to Dutch
npx po-genie -i languages/messages.pot -l nl_NL

# Translate an existing .po file in-place (only fills missing strings)
npx po-genie -i languages/messages-nl_NL.po -l nl_NL

# Multiple locales at once
npx po-genie -i languages/messages.pot -l nl_NL,de_DE,fr_FR

# Translate a WordPress plugin readme
npx po-genie -i readme.txt -l nl_NL

# Specify model and add project context for better quality
npx po-genie -i languages/messages.pot -l nl_NL \
  -m google/gemini-2.0-flash-001 \
  -c "WordPress WooCommerce loyalty plugin"

CLI reference

po-genie [OPTIONS]

  -i, --input       (required) Path to source .po, .pot, or readme .txt file
  -l, --locale      (required) Target locale(s), comma-separated (e.g. nl_NL or nl_NL,de_DE)
  -o, --output      Output path (default: input dir with locale suffix)
  -m, --model       OpenRouter model ID (default: anthropic/claude-3.5-haiku)
  -c, --context     Project context sent to the AI for better quality
  --batch-size      Strings per AI request (default: 40)
  --concurrency     Maximum locale translations run at once (default: 2)
  --all-strings     Re-translate all gettext strings, not just missing or fuzzy ones

Batch size and concurrency values must be positive integers.

Input and output behaviour

| Input | Locale | Output | |---|---|---| | messages.pot | nl_NL | messages-nl_NL.po (same dir) | | messages-nl_NL.po | nl_NL | messages-nl_NL.po (in-place) | | readme.txt | nl_NL | readme-nl_NL.txt (same dir) | | any supported file | one locale | exact file from --output | | any supported file | multiple locales | one locale-suffixed file in the existing --output directory |

By default only entries that need work are sent to the model: empty translations and entries flagged fuzzy (whose source text changed since they were translated). Up-to-date translations are skipped, so re-running after a msgmerge only pays for what changed. A successful run clears the fuzzy flag and the #| previous-source comments.

Without --output, multi-locale files are written beside the input. Results keep the requested locale order even when locale jobs finish out of order. If one locale fails, already-started jobs are allowed to settle, no new jobs are started, successful outputs are reported, and the CLI exits unsuccessfully.

Locales outside the built-in plural rules

po-genie ships GNU gettext plural rules for the common locales. For any other locale (for example zh_CN, ar, hi_IN), give the input catalog a Language header for that locale and a valid Plural-Forms header, and po-genie uses it:

"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"

The built-in rule always wins for locales po-genie already knows.

WordPress plugin & theme workflow

If you use WP-CLI's wp i18n commands, po-genie fits naturally into your existing i18n workflow.

1. Generate your .pot file (if you don't already have one):

wp i18n make-pot . languages/my-plugin.pot --slug=my-plugin --domain=my-plugin

2. Add your API key — create a .env file in your plugin/theme root:

# .env  (add this to .gitignore!)
OPENROUTER_API_KEY=sk-or-...

3. Translate:

npx po-genie -i languages/my-plugin.pot -l nl_NL,de_DE,fr_FR \
  -c "WordPress plugin for [what your plugin does]"

This generates languages/my-plugin-nl_NL.po, languages/my-plugin-de_DE.po, and languages/my-plugin-fr_FR.po.

4. Translate the plugin readme, if present:

npx po-genie -i readme.txt -l nl_NL,de_DE

This generates readme-nl_NL.txt, readme-de_DE.txt, etc. Plugin metadata, version headings, and code blocks remain unchanged.

5. Compile catalogs to .mo:

wp i18n make-mo languages/

Tip — add it to your package.json scripts so the whole flow is one command:

{
  "scripts": {
    "i18n": "wp i18n make-pot . languages/my-plugin.pot --slug=my-plugin --domain=my-plugin",
    "i18n:translate": "po-genie -i languages/my-plugin.pot -l nl_NL,de_DE -c \"My plugin description\"",
    "i18n:compile": "wp i18n make-mo languages/"
  }
}
pnpm i18n && pnpm i18n:translate && pnpm i18n:compile

In-place PO updates are safe by default. When the input is an already locale-suffixed .po written back in place, the default onlyMissing: true preserves completed translation slots and fills only required empty slots. Re-running a .pot or readme.txt translation regenerates its target output and can make new provider requests with additional cost. Matching checkpoints separately resume validated work from an interrupted run.

Programmatic API

import { translate } from 'po-genie'

const results = await translate({
  input: './languages/messages.pot',
  locale: ['nl_NL', 'de_DE'],
  model: 'google/gemini-2.0-flash-001',
  context: 'WordPress e-commerce plugin',
  batchSize: 40,
  concurrency: 2,
  onProgress: ({ locale, translated, total, batch, batches }) => {
    console.log(`${locale}: ${translated}/${total} (${batch}/${batches})`)
  },
})

for (const result of results) {
  console.log(result.locale, result.output, result.usage.totalTokens)
}

Options

| Option | Type | Default | Description | |---|---|---|---| | input | string | — | Path to source .po, .pot, or WordPress readme .txt file | | locale | string \| string[] | — | Target locale(s) | | output | string | auto | Exact output file for one locale; existing output directory for multiple locales | | model | string | anthropic/claude-3.5-haiku | OpenRouter model ID | | apiKey | string | OPENROUTER_API_KEY env | OpenRouter API key | | context | string | — | Project description for the AI | | batchSize | positive integer | 40 | Translation jobs per provider request | | concurrency | positive integer | 2 | Maximum locale translations running at once | | onlyMissing | boolean | true | For gettext, preserve completed plural slots and translate only required empty slots | | onProgress | (progress: Progress) => void | — | Called after each validated, checkpointed batch |

Results, progress, and failures

interface Progress {
  locale: string
  translated: number
  total: number
  batch: number
  batches: number
}

interface TranslateResult {
  locale: string
  output: string
  translated: number
  skipped: number
  usage: {
    promptTokens: number
    completionTokens: number
    totalTokens: number
    estimatedCostUsd?: number
  }
}

For gettext, translated and total count catalog entries, including an entry only after all of its required plural forms are complete. skipped counts catalog entries not selected. For a readme, the counts refer to translatable segments and skipped is always zero. Resumed work is included in translated and usage; unavailable usage from failed provider attempts cannot be included.

Every API result includes known token usage. estimatedCostUsd is optional. The CLI prints aggregate token totals when multiple locale results are returned or when at least one known cost estimate is available; it displays cost only when an estimate is known.

If any locale job fails, translate() throws LocaleTranslationError after every started job settles, including when only one locale was requested. For multiple locales, the error separates completed partial results, failed locales, and locales that were not started:

import { LocaleTranslationError, translate } from 'po-genie'

try {
  await translate({
    input: './languages/messages.pot',
    locale: ['nl_NL', 'de_DE', 'fr_FR'],
  })
} catch (error) {
  if (error instanceof LocaleTranslationError) {
    console.log(error.successes)        // complete TranslateResult objects
    console.log(error.failures)         // Array<{ locale: string, reason?: string }>
    console.log(error.unstartedLocales) // locale strings
  }
}

Failure details may include a safe validation summary or provider HTTP status. They never expose provider request or response content. Completed locale outputs remain usable.

Checkpoints and output safety

After each validated batch, po-genie atomically writes a checkpoint beside the planned output:

<output>.po-genie-checkpoint.json

Re-running the same translation resumes completed work and accumulated known usage automatically. The source contents, locale, pipeline, model, batch size, onlyMissing, and project context must still match. A corrupt or mismatched checkpoint is left untouched and produces an error explaining that it must be removed to restart.

On success, the final PO or readme is replaced atomically and its checkpoint is removed. If the final write fails, an existing output is preserved.

Variable detection

The following patterns are detected, tokenised before translation, and restored after:

| Pattern | Example | |---|---| | Handlebars / custom | {{credits}}, {{credits_currency}} | | Printf | %s, %d, %f | | Positional printf | %1$s, %2$d | | Python-style | %(name)s | | Single-brace | {variable} |

Models and cost estimates

Pass any model ID supported by your OpenRouter account. Token counts are included in API results for successful provider responses. estimatedCostUsd is included only for model IDs in po-genie's built-in price table; it is an estimate, not a billing guarantee. Model availability and pricing change, so use OpenRouter's current model page and your provider bill as the authority.

License

MIT