msgai is an AI-powered CLI for translating gettext .po files. It finds untranslated entries, sends them to an LLM, and writes the translated strings back into the same file.
msgai is built for teams that already use gettext and want a simple way to translate missing strings without building a separate localization workflow.
Main features:
📝Works directly with gettext.pofiles🤖Translates only untranslated entries using AI🧠Uses OpenAIgpt-5.4by default for translation🏷️Respects gettext context (msgctxt) when translating entries🔁Supports singular and plural translations⚠️Skips fuzzy entries by default🪪Marks every AI translation with a# ai-translatedtranslator comment🧭Can infer source language or use--source-lang💻Runs as a small CLI that updates files in place
- Read the
.pofile and parse its entries. - Find entries with empty or missing translations.
- Send those strings to OpenAI
gpt-5.4for translation while preserving gettext context such asmsgctxt. - Write the translated values back into the same
.pofile.
The translation API uses OpenAI json_schema structured outputs. Only models that support json_schema structured outputs are valid for msgai.
Any OpenAI model that supports json_schema structured outputs can be used via the --model flag.
By default, entries marked as fuzzy are skipped. If you use --include-fuzzy, msgai will translate those entries too and remove the fuzzy flag after applying the result.
Every entry that msgai translates gets a # ai-translated translator comment so you can tell AI translations apart from human ones. Existing translator comments are preserved. Use --add-fuzzy to additionally mark fresh translations with the gettext fuzzy flag — useful when you want a human to review every AI translation before it ships.
Install the CLI globally:
npm install -g msgai-cliSet your OpenAI API key before running translations:
export OPENAI_API_KEY=your_api_key_hereYou can also pass the key directly:
msgai messages.po --api-key sk-...OPENAI_API_KEY can be loaded from your environment or from a .env file in the current directory.
Usage:
msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--add-fuzzy] [--fold-length N] [--context TEXT] [--config PATH] [--debug]Options:
--dry-run: list untranslatedmsgidvalues only, with no API calls and no file changes--include-fuzzy: include fuzzy entries for translation and clear their fuzzy flag after translation--add-fuzzy: mark every newly translated entry with the gettextfuzzyflag (so a human reviews it before it ships). Independent of--include-fuzzy--source-lang LANG: set the source language ofmsgidstrings as an ISO 639-1 code such asenoruk--model MODEL: set the OpenAI model used for translation; default isgpt-5.4. Only models withjson_schemastructured outputs are supported.--api-key KEY: pass the OpenAI API key directly instead of usingOPENAI_API_KEY--fold-length N: set PO line fold length when writing files. Use0to disable folding and minimize formatting-only diffs. Default:0--context TEXT: additional instructions for the translation model in English, appended to the system prompt (e.g. "use formal tone", "don't translate currency names")--config PATH: path to a YAML config file (default:msgai.config.ymlin current directory)--debug: print debug logs for batch preparation, OpenAI request retries, request payloads, and raw response validation--help: print command usage
You can also enable the same debug logging with the environment variable DEBUG=1:
DEBUG=1 msgai messages.pomsgai supports an optional msgai.config.yml config file in the project directory. If found, its values are used as defaults. CLI arguments always override config file values.
Use --config PATH to specify a custom config file location. If --config is not provided, msgai looks for msgai.config.yml in the current working directory.
Example msgai.config.yml:
source-lang: en
model: gpt-5.4
include-fuzzy: false
add-fuzzy: false
fold-length: 80
context: "use formal tone"
debug: falseBoth kebab-case and camelCase keys are accepted.
api-key and dry-run are not allowed in the config file. API keys should be set via --api-key flag or OPENAI_API_KEY environment variable for security reasons. dry-run is a runtime-only option that must be passed as a CLI flag.
If no API key is provided for a non-dry run, the CLI exits with code 1 and prints an error message.
On API failures such as rate limits, quota issues, or server errors, the CLI exits with code 1 and shows a status-specific message. Validation errors for protected fields such as msgid, msgid_plural, or msgctxt now tell you whether a retry is reasonable and when to rerun with --debug or DEBUG=1 to inspect the request/response flow. For API error details, see OpenAI API error codes.
Requirements:
- Node.js
20+ - npm
10+
Install dependencies:
npm installUseful scripts:
npm run build: compile TypeScript todist/npm test: build the project and run Jest testsnpm run test:integration: run integration testsnpm run test:watch: run tests in watch modenpm run lint: run ESLintnpm run lint:format: check formatting with Prettiernpm run format: format the repository with Prettiernpm run release:dry-run: preview thecommit-and-tag-versionrelease without writing filesnpm run release: run release checks, updateCHANGELOG.md, bump the npm version, create a release commit, and create a local tag
This repo follows Conventional Commits for commit messages.
Maintainer releases are local-first and use commit-and-tag-version. The release command does not publish to npm or push tags for you.
Preview the next release:
npm run release:dry-runCreate the release locally:
npm run releaseThis command:
- runs
build, unit tests, integration tests, lint, and formatting checks through theprereleaselifecycle hook - lets
commit-and-tag-versioninfermajor,minor, orpatchfrom Conventional Commits since the latestv*tag - updates
CHANGELOG.md - creates
chore(release): X.Y.Z - creates a local annotated tag
vX.Y.Z
For reliable version bumps and changelog entries, keep commits in Conventional Commit format.
If you need to override the inferred bump manually:
npm run release -- --release-as minorAfter the local release is created:
git push --follow-tags
npm publish