Strict-typed, dependency-free gettext-compatible i18n library (PO/MO parser, Plural-Forms evaluator, Catalog API).
Project description
pomo-i18n
English | 日本語
pomo-i18n is a lightweight Python library for working with GNU gettext translation data,
centered around a practical and explicit Catalog model.
Version 0.2.0 focuses on providing a gettext-compatible, in-memory translation catalog
with a dict-like API, while keeping interoperability
with traditional gettext workflows (.po, .mo, gettext(), ngettext()).
Overview
pomo-i18n is designed for developers who want to:
- Work with gettext translation data without relying on global state
- Treat translations as explicit Python objects
- Use gettext-compatible behavior while staying idiomatic in Python
At the core of the library is the Catalog class -- an in-memory
representation of a gettext message catalog.
A Catalog:
- Stores messages keyed by
msgid - Treats the header (
msgid == "") as first-class data - Provides a dict-like interface compatible with gettext semantics
- Supports plural rules, plural evaluation and caching
- Can be loaded from
.po/.mofiles and written back to.mo
Unlike the standard gettext module, pomo-i18n does not hide state behind
module-level globals. Instead, all translation data lives in explicit Catalog
instances, making the behavior predictable, testable, and composable.
Scope and Non-goals
pomo-i18n focuses on explicit, in-memory gettext catalogs.
It intentionally does NOT provide:
- Global translation state
- Automatic language switching
- Framework-specific integrations (Django, Flask, etc.)
These are expected to be built on top of Catalog.
gettext Compatibility
pomo-i18n aims to be behavior-compatible with GNU gettext where it matters:
- Missing translations fall back to
msgid - Header handling follows gettext conventions
gettext()andngettext()APIs are provided- Plural rules are evaluated according to the gettext specification
At the same time, pomo-i18n exposes these concepts through a clear object model,
allowing direct inspection and manipulation of translation data.
Installation
pip install pomo-i18n
Requirements:
- Python 3.10+
GNU gettext dependency
pomo-i18n does not require GNU gettext at runtime. Only Python is required.
All runtime operations — loading .mo files, evaluating plural rules,
and performing gettext() / ngettext() lookups — are implemented
entirely in Python.
This makes pomo-i18n usable on platforms such as Windows without installing external GNU gettext tools.
.po files are currently supported as an import format.
Full .po -> .mo compilation support is planned for a future release.
Quick Start
The central object in pomo-i18n is the Catalog.
A Catalog represents an in-memory gettext message catalog and can be used
much like a Python dictionary.
Create a Catalog
from pypomo.catalog import Catalog
catalog = Catalog()
Dict-like usage
You can assign and retrieve translations using familiar dictionary syntax.
catalog["hello"] = "こんにちは"
print(catalog["hello"])
# -> "こんにちは"
If a translation is missing, the msgid itself is returned
(gettext-compatible behavior):
print(catalog["missing"])
# -> "missing"
The gettext header (msgid == "") is treated as first-class data:
catalog[""] = (
"Language: ja\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
)
print(catalog[""])
# -> header string
Using get()
Catalog.get() behaves similarly to dict.get() with gettext-compatible fallback behavior:
catalog.get("hello")
# -> CatalogMessage
catalog.get("missing")
# -> "missing"
catalog.get("missing", "fallback")
# -> "fallback"
gettext / ngettext APIs
Catalog also provides familiar gettext-style APIs:
catalog.gettext("hello")
# -> "こんにちは"
catalog.ngettext("apple", "apples", 1)
# -> "apple"
catalog.ngettext("apple", "apples", 2)
# -> "apples"
Plural evaluation follows the Plural-Forms rule defined in the catalog header.
If no rule is defined, a gettext-compatible default is used.
Loading and Writing .po / .mo
A Catalog can be created from .po (import) or .mo files and written back to .mo format:
from pypomo.mo.loader import load_mo
from pypomo.mo.writer import write_mo
catalog = load_mo("messages.mo")
write_mo("messages_out.mo", catalog)
(See the sections below for details.)
Design Philosophy: Why Catalog?
The design of pomo-i18n is driven by one core principle:
gettext compatibility without hidden global state
Problems with the standard gettext module
Python’s built-in gettext module is powerful, but it comes with trade-offs:
- Translation state is stored in module-level globals
- Active language and domain are often implicit
- Testing requires patching or reconfiguring global state
- Inspecting or modifying translation data is difficult
This makes gettext awkward in modern Python applications where:
- Explicit dependencies are preferred
- Multiple catalogs may coexist
- Deterministic behavior and testability matter
Catalog as an explicit translation object
pomo-i18n replaces implicit global state with an explicit object model.
A Catalog is:
- A concrete, inspectable Python object
- An in-memory representation of a gettext message catalog
- The single source of truth for:
- messages
- header metadata
- plural rules
- language information
Instead of asking “what is the current translation?”, you work with “this specific catalog instance”.
catalog = Catalog()
catalog["hello"] = "こんにちは"
Dict-like API with gettext semantics
A key design goal of Catalog is to feel natural to Python users
while remaining faithful to gettext behavior.
Example:
- Missing keys fall back to
msgid - The header (
msgid == "") is a first-class entry - No
KeyErroris raised for missing translations - Plural behavior follows
Plural-Formsrules
catalog["missing"]
# -> "missing"
This mirrors how gettext behaves, but through a familiar dictionary-style interface.
Separation of concerns
pomo-i18n intentionally separates responsibilities:
-
Catalog -
Owns message data, header state, and plural rules
-
CatalogMessage -
Represents a single gettext entry (singular, plural, translations)
-
.po/.moloaders and writersHandle file formats, not runtime behavior
This separation allows:
- Clean internal APIs
- Easier refactoring
- Future extensions (e.g. merge strategies, plural dict access)
Designed for extensions, not hidden behavior
pomo-i18n is designed around explicit objects and predictable behavior,
rather than hidden state or implicit side effects.
- No implicit language switching
- No hidden registries
- No global translation state
Instead, it provides building blocks that can be composed to fit different application architectures.
This makes the library suitable for:
- CLI tools
- Web frameworks
- Embedded systems
- Testing environments
- Custom i18n pipelines
Summary
In short,
Catalogreplaces implicit gettext state with explicit objects- Dict-like APIs make translation access intuitive
- gettext compatibility is preserved where it matters
- The design favors clarity, predictability, and extensibility
This philosophy shapes all APIs in pomo-i18n starting with Catalog
and expanding outward.
Dict-like API details (v0.2.0)
Catalog provides a dictionary-like interface that follows gettext-compatible semantics
while remaining explicit and Pythonic.
This API is the primary interface of pomo-i18n v0.2.0.
catalog[key] -- lookup
catalog["hello"]
Behavior:
| Condition | Result |
|---|---|
| key exists | translated singular string |
| key missing | returns key itself |
key == "" |
returns header msgstr |
key is not str |
TypeError |
Example:
catalog["hello"] = "こんにちは"
catalog["hello"] # -> "こんにちは"
catalog["missing"] # -> "missing"
catalog[""] # -> `header string`
Notes:
- Never raises
KeyError - Fully compatible with gettext fallback behavior
- Plural forms are not handled here (v0.2.0)
catalog[key] = value -- assignment
catalog["hello"] = "こんにちは"
Behavior:
keymust bestrvaluemust bestr- Creates or replaces a singular message
key == ""updates the catalog header
Example:
catalog[""] = "Language: ja\nPlural-Forms: nplurals=2; plural=(n != 1);\n"
Notes:
- Plural assignment is intentionally not supported in v0.2.0.
- Header assignment updates plural rules if present
"key" in catalog -- membership test
"hello" in catalog
Behavior:
| Condition | Result |
|---|---|
| existing msgid | True |
| missing msgid | False |
key == "" |
True if header exists |
key not str |
False |
Example:
"hello" in catalog
"" in catalog
Iteration and Views
for key in catalog:
...
Equivalent to:
catalog.keys()
Available views:
catalog.keys() # KeysView[str]
catalog.values() # ValuesView[CatalogMessage]
catalog.items() # ItemsView[str, CatalogMessage]
len(catalog) # number of messages (including header)
catalog.get(key[, default])
catalog.get("hello")
catalog.get("hello", default)
Behavior (gettext-compatible):
| Condition | Result |
|---|---|
| key exists | CatalogMessage |
| key missing, default provided | default |
| key missing, no default | returns key |
key not str |
TypeError |
Example:
catalog.get("hello")
# -> CatalogMessage
catalog.get("missing")
# -> "missing"
catalog.get("missing", "fallback")
# -> "fallback"
Design notes:
- Unlike
dict.get(), the no-default case returnskey - This mirrors gettext's fallback semantics
- The returned object is not a string when a message exists
Deletion is not supported
del catalog["hello"]
Always raises:
TypeError
Reason:
- gettext catalogs are append/override-oriented
- Explicit deletion is intentionally unsupported
Design rationale
- No
KeyError-- gettext-compatible behavior - Explicit message objects -- avoids hidden state
- Header as first-class data -- not a special case
- Singular-first API -- plural dict-like API planned for v0.3.0
gettext / ngettext APIs
In addition to its dict-like interface, Catalog provides
gettext-compatible translation APIs.
These APIs are intended for drop-in familiarity with existing gettext-based
code, while still operating on an explicit Catalog interface.
gettext()
catalog.gettext("hello")
# -> "こんにちは"
Behavior:
| Situation | Result |
|---|---|
| Translation exists | Translated string |
| Translation not exists | Original msgid |
msgid == "" (header) |
Not accessed via gettext() |
This matches standard GNU gettext behavior.
ngettext()
catalog.ngettext("apple", "apples", 1)
# -> "apple"
catalog.ngettext("apple", "apples", 2)
# -> "apples"
Behavior:
| Step | Condition | Result |
|---|---|---|
| 1 | No translation exists | singular if n == 1, else plural |
| 2 | Translation exists | Evaluate plural index via Plural-Forms |
| 3 | msgstr[index] exists |
Return that form |
| 4 | Index out of range, msgstr[0] exists |
Fallback to msgstr[0] |
| 5 | No valid translation form found | Fallback to translated singular |
| 6 | Last resort | Fallback to original plural argument |
Plural selection follows GNU gettext semantics.
Plural Rules
Plural rules are defined by the Plural-Forms header:
Plural-Forms: nplurals=2; plural=(n != 1);
| Case | Behavior |
|---|---|
Header contains Plural-Forms |
Rule is parsed, compiled, and cached |
| Header missing | gettext-compatible default is used |
| Default rule | nplurals=2; plural=(n != 1) |
Relationship to the Dict-like API
The dict-like API and gettext-style APIs are complementary
| API | Purpose |
|---|---|
catalog["key"] |
Simple access (singular only) |
catalog.get() |
Introspective access to message objects |
gettext() |
gettext-compatible string lookup |
ngettext() |
Plural-aware gettext-compatible lookup |
In v0.2.0:
- Dict-like access is singular-style
- Plural handling is intentionally explicit via
ngettext()
Design Notes
| Design Choice | Rationale |
|---|---|
| Instance-based API | No hidden global state |
Explicit Catalog object |
Deterministic, testable behavior |
| Separate plural API | Avoid ambiguous dict-like plural semantics |
Future Direction
| Version | Planned Improvement |
|---|---|
| v0.3.x | Dict-like plural support |
| v0.3.x+ | Closer integration with CatalogMessage |
Until then, ngettext() remains the canonical plural API.
CatalogMessage Design
CatalogMessage is the internal message representation used by Catalog.
It corresponds closely to a single gettext entry (msgid, msgstr, msgid_plural, msgstr[n]),
but is normalized into a Python-friendly, immutable-like structure.
Purpose
CatalogMessage exists to:
-
Represent a fully resolved translation unit
-
Separate parsing concerns (
POEntry) from runtime lookup -
Provide a stable object model for:
- dict-like access
- gettext / ngettext evaluation
.mowriting
Unlike POEntry, CatalogMessage is not a file format model --
it represents how translations behave at runtime.
Structure
@dataclass(slots=True)
class CatalogMessage:
msgid: str
singular: str
plural: str | None
translations: Dict[int, str]
Fields:
| Field | Meaning |
|---|---|
msgid |
Logical key (original untranslated string) |
singular |
Primary translation (never empty) |
plural |
Plural msgid or None |
translations |
{plural_index: translated_string} |
Normalization rules
CatalogMessage enforces several invariants in __post_init__:
-
singularis never empty- Falls back to
msgid
- Falls back to
-
plural == ""is normalized toNone -
translations[0]always exists- Falls back to
singular
- Falls back to
-
Empty plural translations fall back conservatively:
- index
0->singular - index
n->pluralorsingular
- index
This guarantees that:
- Lookup code never needs defensive checks
- Missing plural forms degrade safely
.mowriting can assume consistent data
Construction helpers
Singular message
CatalogMessage.from_singular(
msgid="hello",
msgstr="こんにちは"
)
Used by:
catalog["key"] = value- Simple catalogs without plural support
Plural message
CatalogMessage.from_plural(
msgid="apple",
msgid_plural="apples",
forms={
0: "りんご",
1: "りんごたち",
},
)
Used internally when building from .po or mo data.
Missing forms are normalized automatically.
Access helpers
msg.as_plain()
Returns the singular translation (translations[0])
msg.get_plural(index)
Safely retrieves a plural form with fallback.
These helpers are primarily used by:
Catalog.__getitem__Catalog.ngettext.mowriter
Immutability model
CatalogMessage is immutable-like by design:
- Uses
@dataclass(slots=True) - No public mutation API
- Treated as replace-on-write inside
Catalog
This follows:
- Safe reuse across catalogs
- Predictable merge / update semantics
- Efficient caching and plural evaluation
Relationship to other layers
| Layer | Role |
|---|---|
POEntry |
File-format parsing model (.po) |
CatalogMessage |
Runtime message representation |
Catalog |
Lookup, plural evaluation, API surface |
This separation is intentional:
- Parsing (
POEntry) may change in future versions - Runtime behavior (
CatalogMessage) remains stable
Version notes
- v0.2.0 supports singular-first usage
- Plural dict-like assignment is planned for v0.3.0
CatalogMessagealready supports full plural data
Loading and Writing Translation Files
pomo-i18n treats file formats (.po, .mo) as serialization layers
around the central Catalog abstraction.
In v0.2.0, .mo files are the primary runtime format, while .po files are
used as an import format.
Loading .po Files
Text-based .po files can be parsed using POParser
and converted into a Catalog:
from pypomo.parser.po_parser import POParser
from pypomo.catalog import Catalog
parser = POParser()
entries = parser.parse("messages.po")
catalog = Catalog.from_po_entries(entries)
Notes:
-
.poparsing supports:msgidmsgstrmsgid_pluralmsgstr[n]- multiline strings
- comments
-
Advanced features such as
#, fuzzy, flags are collected as comments but not yet interpreted semantically -
.pofiles are treated primarily as an import format in v0.2.0
Loading .mo Files
Binary .mo files can be loaded directly into a Catalog:
from pypomo.mo.loader import load_mo
catalog = load_mo("messages.mo")
This auto:
- Loads all messages into memory
- Parses the header (
msgid == "") - Extracts and evaluates
Plural-Forms - Enables immediate use via:
catalog["msgid"]catalog.gettext()catalog.ngettext()
Writing .mo Files
A Catalog can be written back to a gettext-compatible .mo file:
from pypomo.mo.writer import write_mo
write_mo("messages_out.mo", catalog)
Note:
- Output is fully GNU gettext-compatible
- Header and plural rules are preserved
- Changes made via the dict-like API are reflected in the output
.mois the recommended runtime/export format
Writing .po Files
Writing .po files is not supported v0.2.0.
Support for .po writing and richer metadata handling is planned for
future releases.
Design Notes
Catalogis the single source of truth.po/.mofiles are serialization layers- No hidden global state
- Explicit, testable, composable translation handling
Roadmap (Related)
Planned improvements in upcoming versions include:
- Refactored
.pomodule layout (pypomo.po.*) - Full support for flags (
fuzzy, etc.) .powriting support- Richer merge and update strategies
Roadmap
- v0.3.x
- Dict-like plural access
- Refactored
.pomodule layout - Improved merge strategies
License
MIT License © 2025 Kiminori Kato
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pomo_i18n-0.2.0.tar.gz.
File metadata
- Download URL: pomo_i18n-0.2.0.tar.gz
- Upload date:
- Size: 44.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c99d3e15d552a008b41fac091c52578ad8321f50839b39b58e5765630ca365f
|
|
| MD5 |
2385dddd766c84deddd3929a007e1ba5
|
|
| BLAKE2b-256 |
495d39bd9307e9830016408525e6bee23c40e3ccc6b0112190120f7c1b92400e
|
Provenance
The following attestation bundles were made for pomo_i18n-0.2.0.tar.gz:
Publisher:
release.yml on kimikato/pomo-i18n
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pomo_i18n-0.2.0.tar.gz -
Subject digest:
6c99d3e15d552a008b41fac091c52578ad8321f50839b39b58e5765630ca365f - Sigstore transparency entry: 782523021
- Sigstore integration time:
-
Permalink:
kimikato/pomo-i18n@77c6864599f1bc19b36871dfc94a117c11682207 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/kimikato
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@77c6864599f1bc19b36871dfc94a117c11682207 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pomo_i18n-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pomo_i18n-0.2.0-py3-none-any.whl
- Upload date:
- Size: 8.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d88fdaf22b607a5456bcb22822b5633c4bdcaf9ead7b0472f62d4c2ebbeba679
|
|
| MD5 |
375a37a588bdc1c31bfcb7e4994c87e0
|
|
| BLAKE2b-256 |
ac42fb3204043be41e1a5ab767bb35291e529d3625a457b7c3b5d1fb30e13d9f
|
Provenance
The following attestation bundles were made for pomo_i18n-0.2.0-py3-none-any.whl:
Publisher:
release.yml on kimikato/pomo-i18n
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pomo_i18n-0.2.0-py3-none-any.whl -
Subject digest:
d88fdaf22b607a5456bcb22822b5633c4bdcaf9ead7b0472f62d4c2ebbeba679 - Sigstore transparency entry: 782523023
- Sigstore integration time:
-
Permalink:
kimikato/pomo-i18n@77c6864599f1bc19b36871dfc94a117c11682207 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/kimikato
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@77c6864599f1bc19b36871dfc94a117c11682207 -
Trigger Event:
push
-
Statement type: