pofile

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2026 License: MIT Imports: 15 Imported by: 0

README

pofile

pofile is a Go module for gettext text catalogs: .po and .pot.

It is built for predictable file processing in build tools and localization pipelines, without pulling in runtime gettext behavior.

It provides:

  • semantic parse API (Parse, ParseReader, ParseFile, ParseDir)
  • lossless parse API (ParseDocument*) with source positions
  • parser diagnostics (ParseCatalogWithDiagnostics)
  • formatter/writer (Format, WriteFile, FormatDocument)
  • semantic model (Catalog, Message)
  • lossless model (Document, Entry, Comment, Header)
  • index for fast key lookup in lossless model (NewIndex, EntryKey)
  • lint/validation (LintDocumentWithOptions, ValidateDocument)
  • merge helper (MergeTemplate)

Scope:

  • text .po/.pot only
  • no binary .mo
  • no runtime gettext engine

Install

go get github.com/woozymasta/pofile

Quick Example

package main

import (
    "log"

    "github.com/woozymasta/pofile"
)

func main() {
    catalog, err := pofile.ParseFile("l18n/russian.po")
    if err != nil {
        log.Fatal(err)
    }

    catalog.UpsertMessage("UI_OK", "OK", "Ок")
    catalog.SetHeader("Last-Translator", "team@example.com")

    if err := pofile.WriteFile("l18n/russian.po", catalog); err != nil {
        log.Fatal(err)
    }
}

Diagnostics Example

catalog, diagnostics, err := pofile.ParseCatalogWithDiagnostics(
    data,
    pofile.ParseOptions{AllowInvalid: true},
)
if err != nil {
    return err
}
_ = catalog
for _, d := range diagnostics {
    // d.Code, d.Severity, d.Position, d.Span
}

Lint Rules

LintDocument and LintDocumentWithOptions emit diagnostics with codes:

  • PO2000: input document is nil
  • PO2001: duplicate domain/context/msgid entry
  • PO2002: msgstr[n] exists, but msgid_plural is missing
  • PO2003: translations exist, but Language header is empty
  • PO2004: entry has empty msgid
  • PO2005: printf-like placeholders mismatch between source and translation
  • PO2006: msgid_plural exists, but msgstr[n>0] is missing

Severity policy:

  • basic mode: non-critical checks are warnings
  • strict mode: warnings are upgraded to errors

Documentation

Overview

Package pofile provides primitives for gettext text catalogs (.po/.pot).

The package exposes two data layers:

  • Catalog for semantic operations (upsert/find/delete, headers, merge)
  • Document for lossless workflows (comments, ordering, source positions)

Use ParseFile or ParseDir for semantic flows. Use ParseDocumentFile and ParseDocumentDir when you need source-preserving round-trip behavior. Use ParseCatalogWithDiagnostics for tolerant parse with structured findings.

Scope is intentionally narrow:

  • text .po/.pot only
  • no binary .mo support
  • no runtime gettext lookup engine

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilCatalog indicates that a catalog argument is nil.
	ErrNilCatalog = errors.New("catalog is nil")

	// ErrNilDocument indicates that a document argument is nil.
	ErrNilDocument = errors.New("document is nil")

	// ErrTemplateRequired indicates that template catalog is missing.
	ErrTemplateRequired = errors.New("template catalog is required")

	// ErrNilMessage indicates that message list contains a nil item.
	ErrNilMessage = errors.New("message is nil")

	// ErrMessageIDRequired indicates message with empty msgid.
	ErrMessageIDRequired = errors.New("message id is required")

	// ErrDuplicateMessage indicates duplicate context+id pair.
	ErrDuplicateMessage = errors.New("duplicate message")

	// ErrDuplicateEntryKey indicates duplicate domain+context+id in document index.
	ErrDuplicateEntryKey = errors.New("duplicate entry key")
)

Functions

func Format

func Format(catalog *Catalog) ([]byte, error)

Format encodes semantic catalog into PO/POT text bytes.

func FormatDocument

func FormatDocument(document *Document, options *WriteOptions) ([]byte, error)

FormatDocument encodes document into PO/POT text.

func FormatWithOptions

func FormatWithOptions(catalog *Catalog, options *WriteOptions) ([]byte, error)

FormatWithOptions encodes semantic catalog using document write options.

func ParseCatalogWithDiagnostics

func ParseCatalogWithDiagnostics(
	data []byte,
	options ParseOptions,
) (*Catalog, []Diagnostic, error)

ParseCatalogWithDiagnostics decodes PO/POT bytes and returns diagnostics.

func ParseDir

func ParseDir(dir string) (map[string]*Catalog, error)

ParseDir loads all *.po files from directory indexed by file base name.

func ParseDocumentDir

func ParseDocumentDir(dir string) (map[string]*Document, error)

ParseDocumentDir loads all *.po files from directory indexed by file base name.

func ParseDocumentReaderWithOptions

func ParseDocumentReaderWithOptions(
	reader io.Reader,
	options ParseOptions,
) (*Document, []Diagnostic, error)

ParseDocumentReaderWithOptions parses reader and returns diagnostics.

func ParseDocumentWithOptions

func ParseDocumentWithOptions(
	data []byte,
	options ParseOptions,
) (*Document, []Diagnostic, error)

ParseDocumentWithOptions parses PO/POT bytes and returns diagnostics.

func ValidateDocument

func ValidateDocument(document *Document) error

ValidateDocument runs strict lint and returns one aggregated error.

func WriteDocumentFile

func WriteDocumentFile(path string, document *Document, options *WriteOptions) error

WriteDocumentFile formats document and writes file to disk.

func WriteFile

func WriteFile(path string, catalog *Catalog) error

WriteFile encodes semantic catalog and writes to file path.

func WriteFileWithOptions

func WriteFileWithOptions(path string, catalog *Catalog, options *WriteOptions) error

WriteFileWithOptions encodes semantic catalog and writes with options.

Types

type Catalog

type Catalog struct {
	// Headers stores header values from the initial msgid/msgstr block.
	Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`

	// Language is copied from "Language" header when present.
	Language string `json:"language,omitempty" yaml:"language,omitempty"`

	// Messages stores translation units.
	Messages []*Message `json:"messages,omitempty" yaml:"messages,omitempty"`
}

Catalog is an in-memory PO/POT semantic model.

func MergeTemplate

func MergeTemplate(template, existing *Catalog) (*Catalog, error)

MergeTemplate applies template messages and keeps existing translations.

func NewCatalog

func NewCatalog() *Catalog

NewCatalog creates an empty catalog.

func Parse

func Parse(data []byte) (*Catalog, error)

Parse decodes PO/POT bytes into semantic catalog.

func ParseFile

func ParseFile(path string) (*Catalog, error)

ParseFile opens and parses a PO/POT file into semantic catalog.

func ParseReader

func ParseReader(reader io.Reader) (*Catalog, error)

ParseReader decodes PO/POT stream into semantic catalog.

func (*Catalog) Clone

func (c *Catalog) Clone() *Catalog

Clone makes a deep copy of catalog.

func (*Catalog) ContentHash

func (c *Catalog) ContentHash() string

ContentHash returns deterministic hash for effective catalog content.

func (*Catalog) DeleteMessage

func (c *Catalog) DeleteMessage(context, id string) bool

DeleteMessage removes message by context+id in default domain.

func (*Catalog) DeleteMessageInDomain

func (c *Catalog) DeleteMessageInDomain(domain, context, id string) bool

DeleteMessageInDomain removes message by domain+context+id.

func (*Catalog) FindMessage

func (c *Catalog) FindMessage(context, id string) *Message

FindMessage returns message by context+id in default domain.

func (*Catalog) FindMessageInDomain

func (c *Catalog) FindMessageInDomain(domain, context, id string) *Message

FindMessageInDomain returns message by domain+context+id, or nil when missing.

func (*Catalog) Header

func (c *Catalog) Header(key string) string

Header returns one header value, or empty string when missing.

func (*Catalog) IsTranslated

func (c *Catalog) IsTranslated(context, id string) bool

IsTranslated reports whether singular translation is non-empty in default domain.

func (*Catalog) IsTranslatedInDomain

func (c *Catalog) IsTranslatedInDomain(domain, context, id string) bool

IsTranslatedInDomain reports whether singular translation is non-empty.

func (*Catalog) MarshalText

func (c *Catalog) MarshalText() ([]byte, error)

MarshalText encodes catalog into PO/POT bytes.

func (*Catalog) SetHeader

func (c *Catalog) SetHeader(key, value string)

SetHeader sets or replaces one header key.

func (*Catalog) Translation

func (c *Catalog) Translation(context, id string) string

Translation returns singular translation by context+id in default domain.

func (*Catalog) TranslationInDomain

func (c *Catalog) TranslationInDomain(domain, context, id string) string

TranslationInDomain returns singular translation by domain+context+id.

func (*Catalog) TranslationN

func (c *Catalog) TranslationN(context, id string, index int) string

TranslationN returns plural translation by index in default domain.

func (*Catalog) TranslationNInDomain

func (c *Catalog) TranslationNInDomain(
	domain, context, id string,
	index int,
) string

TranslationNInDomain returns plural translation by index.

func (*Catalog) UnmarshalText

func (c *Catalog) UnmarshalText(data []byte) error

UnmarshalText decodes semantic catalog from PO/POT text bytes.

func (*Catalog) UpsertMessage

func (c *Catalog) UpsertMessage(context, id, translation string) *Message

UpsertMessage upserts one singular message in default domain.

func (*Catalog) UpsertMessageInDomain

func (c *Catalog) UpsertMessageInDomain(
	domain, context, id, translation string,
) *Message

UpsertMessageInDomain upserts one singular message by domain+context+id.

func (*Catalog) Validate

func (c *Catalog) Validate() error

Validate checks catalog structural consistency.

type Comment

type Comment struct {
	Kind     CommentKind `json:"kind" yaml:"kind"`
	Text     string      `json:"text" yaml:"text"`
	Raw      string      `json:"raw" yaml:"raw"`
	Position Position    `json:"position" yaml:"position"`
}

Comment stores typed PO comment line.

type CommentKind

type CommentKind string

CommentKind classifies PO comment prefixes.

const (
	// CommentTranslator is a regular translator comment "#".
	CommentTranslator CommentKind = "translator"

	// CommentExtracted is an extracted/source comment "#.".
	CommentExtracted CommentKind = "extracted"

	// CommentReference is a source reference comment "#:".
	CommentReference CommentKind = "reference"

	// CommentFlags is a flags comment "#,".
	CommentFlags CommentKind = "flags"

	// CommentPrevious is previous-value comment "#|".
	CommentPrevious CommentKind = "previous"

	// CommentOther covers unknown comment styles.
	CommentOther CommentKind = "other"
)

type Diagnostic

type Diagnostic struct {
	Severity Severity `json:"severity" yaml:"severity"`
	Code     string   `json:"code" yaml:"code"`
	Message  string   `json:"message" yaml:"message"`
	Position Position `json:"position" yaml:"position"`
	Span     Span     `json:"span" yaml:"span"`
}

Diagnostic describes one parser/linter issue.

func LintDocument

func LintDocument(document *Document) []Diagnostic

LintDocument runs basic semantic checks and returns diagnostics.

func LintDocumentWithOptions

func LintDocumentWithOptions(
	document *Document,
	options *LintOptions,
) []Diagnostic

LintDocumentWithOptions runs lint with selected checks.

type Document

type Document struct {
	HeaderComments []Comment `json:"header_comments,omitempty" yaml:"header_comments,omitempty"`
	Headers        []Header  `json:"headers,omitempty" yaml:"headers,omitempty"`
	Entries        []*Entry  `json:"entries,omitempty" yaml:"entries,omitempty"`
}

Document keeps parsed PO/POT in source order.

func DocumentFromCatalog

func DocumentFromCatalog(catalog *Catalog) *Document

DocumentFromCatalog converts semantic catalog to document.

func NewDocument

func NewDocument() *Document

NewDocument creates an empty document.

func ParseDocument

func ParseDocument(data []byte) (*Document, error)

ParseDocument parses PO/POT bytes into lossless document.

func ParseDocumentFile

func ParseDocumentFile(path string) (*Document, error)

ParseDocumentFile opens and parses one PO/POT file into document.

func ParseDocumentReader

func ParseDocumentReader(reader io.Reader) (*Document, error)

ParseDocumentReader parses PO/POT from reader into lossless document.

func (*Document) HeaderValue

func (d *Document) HeaderValue(key string) string

HeaderValue returns header value by key, or empty string.

func (*Document) ToCatalog

func (d *Document) ToCatalog() (*Catalog, error)

ToCatalog converts lossless document to semantic catalog.

type Entry

type Entry struct {

	// Translations stores msgstr values by plural index (0 for singular).
	Translations map[int]string `json:"translations,omitempty" yaml:"translations,omitempty"`
	Domain       string         `json:"domain,omitempty" yaml:"domain,omitempty"`
	Context      string         `json:"context,omitempty" yaml:"context,omitempty"`
	ID           string         `json:"id,omitempty" yaml:"id,omitempty"`
	IDPlural     string         `json:"id_plural,omitempty" yaml:"id_plural,omitempty"`

	// Previous values come from "#| msgctxt/msgid/msgid_plural" comments.
	PreviousContext  string `json:"previous_context,omitempty" yaml:"previous_context,omitempty"`
	PreviousID       string `json:"previous_id,omitempty" yaml:"previous_id,omitempty"`
	PreviousIDPlural string `json:"previous_id_plural,omitempty" yaml:"previous_id_plural,omitempty"`

	Comments   []Comment `json:"comments,omitempty" yaml:"comments,omitempty"`
	Flags      []string  `json:"flags,omitempty" yaml:"flags,omitempty"`
	References []string  `json:"references,omitempty" yaml:"references,omitempty"`

	Position Position `json:"position" yaml:"position"`
	Obsolete bool     `json:"obsolete,omitempty" yaml:"obsolete,omitempty"`
}

Entry stores one PO translation unit.

type EntryKey

type EntryKey struct {
	Domain  string `json:"domain,omitempty" yaml:"domain,omitempty"`
	Context string `json:"context,omitempty" yaml:"context,omitempty"`
	ID      string `json:"id" yaml:"id"`
}

EntryKey identifies one PO entry by domain, context, and id.

type Header struct {
	Key      string   `json:"key" yaml:"key"`
	Value    string   `json:"value" yaml:"value"`
	Position Position `json:"position" yaml:"position"`
}

Header stores one parsed header key/value with source position.

type Index

type Index struct {
	// contains filtered or unexported fields
}

Index provides fast key-based lookup for a Document.

func NewIndex

func NewIndex(document *Document) (*Index, error)

NewIndex builds a lookup index for document entries.

func (*Index) Entry

func (i *Index) Entry(key EntryKey) *Entry

Entry returns entry by full key, or nil when missing.

func (*Index) EntryDefaultDomain

func (i *Index) EntryDefaultDomain(context, id string) *Entry

EntryDefaultDomain returns entry in default domain.

func (*Index) EntryInDomain

func (i *Index) EntryInDomain(domain, context, id string) *Entry

EntryInDomain returns entry by domain, context, and id.

type LintMode

type LintMode string

LintMode defines lint strictness profile.

const (
	// LintModeBasic keeps non-critical findings as warnings.
	LintModeBasic LintMode = "basic"

	// LintModeStrict upgrades warnings to errors.
	LintModeStrict LintMode = "strict"
)

type LintOptions

type LintOptions struct {
	// CheckLanguageHeader enables Language header check for translated files.
	CheckLanguageHeader *bool `json:"check_language_header,omitempty" yaml:"check_language_header,omitempty"`

	// CheckPluralShape enables plural consistency checks.
	CheckPluralShape *bool `json:"check_plural_shape,omitempty" yaml:"check_plural_shape,omitempty"`

	// CheckPlaceholders enables printf-like placeholder checks.
	CheckPlaceholders *bool `json:"check_placeholders,omitempty" yaml:"check_placeholders,omitempty"`

	// Mode controls warning/error severity policy.
	Mode LintMode `json:"mode,omitempty" yaml:"mode,omitempty"`
}

LintOptions controls lint checks and strictness.

type Message

type Message struct {
	// Translations stores msgstr values by plural index.
	// Index 0 is the singular translation.
	Translations map[int]string `json:"translations,omitempty" yaml:"translations,omitempty"`

	// Domain stores optional gettext domain.
	Domain string `json:"domain,omitempty" yaml:"domain,omitempty"`

	// Context corresponds to msgctxt.
	Context string `json:"context,omitempty" yaml:"context,omitempty"`

	// ID corresponds to msgid.
	ID string `json:"id,omitempty" yaml:"id,omitempty"`

	// IDPlural corresponds to msgid_plural.
	IDPlural string `json:"id_plural,omitempty" yaml:"id_plural,omitempty"`

	// Previous values come from "#|" comments.
	PreviousContext string `json:"previous_context,omitempty" yaml:"previous_context,omitempty"`

	// PreviousID is previous msgid from "#| msgid".
	PreviousID string `json:"previous_id,omitempty" yaml:"previous_id,omitempty"`

	// PreviousIDPlural is previous msgid_plural from "#| msgid_plural".
	PreviousIDPlural string `json:"previous_id_plural,omitempty" yaml:"previous_id_plural,omitempty"`

	// Comments stores entry comments as raw lines.
	Comments []string `json:"comments,omitempty" yaml:"comments,omitempty"`

	// Flags stores parsed values from "#," comments.
	Flags []string `json:"flags,omitempty" yaml:"flags,omitempty"`

	// References stores parsed values from "#:" comments.
	References []string `json:"references,omitempty" yaml:"references,omitempty"`

	// Obsolete marks an obsolete "#~" entry.
	Obsolete bool `json:"obsolete,omitempty" yaml:"obsolete,omitempty"`
}

Message is one translation entry.

func (*Message) HasFlag

func (m *Message) HasFlag(flag string) bool

HasFlag reports whether message has a given flag in "#," comment.

func (*Message) IsPlural

func (m *Message) IsPlural() bool

IsPlural reports whether message has plural source or plural translations.

func (*Message) SetTranslationAt

func (m *Message) SetTranslationAt(index int, value string)

SetTranslationAt sets translation value by plural index.

func (*Message) TranslationAt

func (m *Message) TranslationAt(index int) string

TranslationAt returns translation value by plural index.

type ParseOptions

type ParseOptions struct {
	// AllowInvalid keeps best-effort parse result with diagnostics.
	AllowInvalid bool `json:"allow_invalid,omitempty" yaml:"allow_invalid,omitempty"`
}

ParseOptions controls parser behavior.

type Position

type Position struct {
	Line   int `json:"line" yaml:"line"`
	Column int `json:"column" yaml:"column"`
	Offset int `json:"offset" yaml:"offset"`
}

Position represents 1-based line/column and byte offset.

type Severity

type Severity string

Severity defines diagnostic level.

const (
	// SeverityError indicates hard parse/validation failure.
	SeverityError Severity = "error"

	// SeverityWarning indicates non-fatal consistency issue.
	SeverityWarning Severity = "warning"

	// SeverityInfo indicates informational message.
	SeverityInfo Severity = "info"
)

type Span

type Span struct {
	StartOffset int `json:"start_offset" yaml:"start_offset"`
	EndOffset   int `json:"end_offset" yaml:"end_offset"`
}

Span represents a byte range in source text.

type WriteMode

type WriteMode string

WriteMode controls output strategy.

const (
	// WriteModePreserve keeps original document order and layout as much as possible.
	WriteModePreserve WriteMode = "preserve"

	// WriteModeCanonical writes deterministic normalized output.
	WriteModeCanonical WriteMode = "canonical"
)

type WriteOptions

type WriteOptions struct {
	// Mode selects preserve or canonical output behavior.
	Mode WriteMode `json:"mode,omitempty" yaml:"mode,omitempty"`

	// HeaderOrder defines preferred key order before generic keys in canonical mode.
	HeaderOrder []string `json:"header_order,omitempty" yaml:"header_order,omitempty"`

	// SortEntries sorts entries by domain/context/id before write.
	SortEntries bool `json:"sort_entries,omitempty" yaml:"sort_entries,omitempty"`

	// SortHeaders sorts headers by key before write.
	SortHeaders bool `json:"sort_headers,omitempty" yaml:"sort_headers,omitempty"`
}

WriteOptions controls document formatting.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL