Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.1.1 - 2026-07-27
Fixed
Catalogue corruption in
translate(data loss). Entries were keyed by msgid alone, but gettext identifies an entry by the pair{msgctxt, msgid}. Any two entries sharing a msgid collapsed onto whichever one won, so a contextlessmsgid "Active"was rewritten to carrymsgctxt "token status". The resulting file has two identical entries andExpo.PO.parse_file!/1refuses to read it, breaking compilation of the whole application.This fired on every write, including a single-string call, and on msgids that were never in the translation input — translating one unrelated string silently collapsed every duplicate-msgid pair in the catalogue.
Silent corruption in
change_msgid. Renaming a msgid onto one the file already used produced an unreadable catalogue and still reported{:ok, ...}. The rename is now refused and the file left untouched.Writer.update_translations/2overwriting the wrong entries. A bare msgid overwrote every entry sharing it, including context-carrying siblings that are distinct translations.
Added
GettextOps.Entry.get_msgctxt/1andGettextOps.Entry.key/1, exposing the{msgctxt, msgid}identity of an entry.nil,[]and""all normalise to "no context", matching Expo.GettextOps.Writer.check_duplicates/1, run before every write so a would-be corruption surfaces as a loud error instead of an unreadable .po file.translatereports an:ambiguouslist for msgids that exist only under two or more different contexts, rather than silently picking one. Without--forcethis is an error; with--forcethey are skipped and listed.msgctxtis included in text and JSON output, sosearchandlist_untranslatedresults distinguish entries that share a msgid.
Changed
translateresolves a bare msgid to the contextless entry when several entries share that msgid. Previously the choice was arbitrary.Writer.update_translations/2accepts{msgctxt, msgid}tuple keys to target a context-carrying entry. A bare msgid string now matches only the contextless entry — it no longer overwrites context-carrying siblings.
0.1.0 - 2025-10-15
Added
- Initial release of gettext_ops
mix gettext_ops.list_untranslated- List entries with empty translationsmix gettext_ops.search- Search for entries by msgid (source text)mix gettext_ops.search_value- Search for entries by msgstr (translated text)mix gettext_ops.translate- Apply translations from stdin or filemix gettext_ops.change_msgid- Update msgid across all locale files- Programmatic API for all operations via
GettextOpsmodule - JSON output support for all commands (line-delimited JSON)
- LLM-friendly stdin/stdout patterns for AI agent workflows
- Token-efficient operations (targeted queries instead of reading entire files)
- Built on Expo library for reliable .po file parsing and writing
- Support for regex and substring search patterns
- Atomic file updates to prevent corruption
- Dry-run mode for
change_msgidcommand - Comprehensive documentation with examples
- Support for custom gettext paths and domains via configuration
Features
- 🎯 Targeted queries - Get only the entries you need
- 📝 Bulk operations - Update multiple translations at once
- 🔄 Global edits - Change msgid across all language files
- 🤖 LLM-friendly - JSON output for easy parsing by AI tools
- ⚡ Fast - Streaming operations with low memory usage
- 🔧 Phoenix-native - Works with standard
priv/gettextstructure