GettextOps.Writer (gettext_ops v0.1.1)

View Source

Module for updating and writing .po files.

This module provides functions for safely updating .po files while preserving structure, comments, and formatting using the Expo library.

Summary

Functions

Changes all occurrences of a msgid to a new msgid in a .po file.

Checks that no two messages share a {msgctxt, msgid} identity.

Updates messages in a .po file using a transformation function.

Updates specific translations in a .po file.

Writes a new .po file with the given messages.

Functions

change_msgid(path, old_msgid, new_msgid)

@spec change_msgid(String.t(), String.t(), String.t()) ::
  {:ok, %{updated: integer()}} | {:error, atom() | Exception.t()}

Changes all occurrences of a msgid to a new msgid in a .po file.

This is useful for refactoring translation keys across the codebase. Every entry with this msgid is renamed, including context-carrying ones — a typo in the source text is a typo in each of its contexts.

If the rename would collide with a msgid the file already uses, the file is left untouched and {:error, reason} is returned; writing it would produce a catalogue that cannot be read back.

Returns {:ok, %{updated: count}} with the number of updated messages.

Examples

# Rename a translation key
GettextOps.Writer.change_msgid("path/to/file.po", "Old text", "New text")
# => {:ok, %{updated: 1}}

check_duplicates(messages)

@spec check_duplicates([Expo.Message.t()]) :: :ok | {:error, String.t()}

Checks that no two messages share a {msgctxt, msgid} identity.

A .po file holding two entries with the same identity cannot be read back — Expo.PO.parse_file!/1 raises Expo.PO.DuplicateMessagesError, which breaks compilation of the whole consuming application. Call this before writing so a bug that would corrupt a catalogue surfaces as a loud error instead.

Returns :ok, or {:error, reason} naming the offending entries.

Examples

messages = [
  %Expo.Message.Singular{msgid: ["Active"]},
  %Expo.Message.Singular{msgid: ["Active"], msgctxt: ["token status"]}
]
GettextOps.Writer.check_duplicates(messages)
# => :ok

update_file(path, update_fn)

@spec update_file(String.t(), (Expo.Message.t() -> Expo.Message.t())) ::
  :ok | {:error, term()}

Updates messages in a .po file using a transformation function.

The update function receives a message and should return the updated message. The original file structure, headers, and comments are preserved.

Examples

# Update all message translations
update_fn = fn msg -> %{msg | msgstr: ["Updated"]} end
GettextOps.Writer.update_file("path/to/file.po", update_fn)
# => :ok

update_translations(path, translations)

@spec update_translations(
  String.t(),
  %{required(String.t() | {String.t() | nil, String.t()}) => String.t()}
) :: {:ok, %{updated: integer()}} | {:error, term()}

Updates specific translations in a .po file.

Accepts a map whose keys identify entries and whose values are the new msgstr. A key may be either:

  • a {msgctxt, msgid} tuple, matching that exact entry; or
  • a plain msgid string, matching only the entry that carries no msgctxt.

A bare msgid deliberately does not match context-carrying entries. Entries that share a msgid but differ in msgctxt are distinct translations, and matching on msgid alone used to overwrite every one of them with the same string. Pass a {msgctxt, msgid} tuple to target a context-carrying entry.

Returns {:ok, %{updated: count}} with the number of updated messages.

Examples

# Update multiple translations
translations = %{"Hello" => "Hej", "Goodbye" => "Hejdå"}
GettextOps.Writer.update_translations("path/to/file.po", translations)
# => {:ok, %{updated: 2}}

# Target a context-carrying entry
translations = %{{"token status", "Active"} => "Giltig"}
GettextOps.Writer.update_translations("path/to/file.po", translations)
# => {:ok, %{updated: 1}}

write_file(path, messages, headers \\ default_headers())

@spec write_file(String.t(), [Expo.Message.t()], [String.t()]) ::
  :ok | {:error, term()}

Writes a new .po file with the given messages.

Creates a new .po file from scratch with the provided messages. Optionally accepts headers as a list of strings.

Examples

# Create a new .po file
messages = [%Expo.Message.Singular{msgid: ["Hello"], msgstr: ["Hej"]}]
GettextOps.Writer.write_file("/tmp/new.po", messages)
# => :ok