What is PO File (Portable Object)?
A PO (Portable Object) file is the translation file format of the gettext system. It is plain text: each entry pairs a source string (msgid) with its translation (msgstr), optionally preceded by comments, source-code references, and flags. Plural entries carry msgid_plural and an indexed set of msgstr forms.
PO files are designed to be edited by humans and diffed in version control, which is why they survived decades of tooling changes. The fuzzy flag marks entries whose source changed and whose translation needs review, an early ancestor of fuzzy matching in translation memories. A header entry carries file metadata, including the language and its plural-forms rule. For runtime use, PO files are compiled into binary MO files.
Even outside gettext-based stacks, PO remains a common interchange format: most TMS platforms and CAT tools import and export it, so it often serves as the bridge between a Laravel project and an external translation vendor.
#: resources/views/dashboard.blade.php:12
msgid "Save changes"
msgstr "Guardar cambios"
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d archivo"
msgstr[1] "%d archivos"
Reading a PO entry
Each entry can carry several kinds of comment, and the character after the # tells you which. #. is an extracted comment: a note from the developer to the translator, copied from the source code. #: lists the source references where the string is used. #, holds flags such as fuzzy or php-format, the latter telling tools that %s and %d are placeholders the translation must keep. #| records the previous msgid of a fuzzy entry when msgmerge runs with --previous.
msgctxt gives two entries with the same msgid separate identities, which is how gettext handles a word like Post that is a verb in one place and a noun in another. The header, the entry with an empty msgid, declares the encoding and the Plural-Forms rule: how many plural forms the language has and the C-style expression that picks one for a given n.
msgid ""
msgstr ""
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Button label on the comment form; submits the comment.
#: resources/views/comments/form.blade.php:18
msgctxt "verb"
msgid "Post"
msgstr "Publicar"
#. Heading above a single item in the feed.
#: resources/views/feed/item.blade.php:4
msgctxt "noun"
msgid "Post"
msgstr "Publicación"
#: app/Http/Controllers/InvoiceController.php:52
#, fuzzy, php-format
msgid "Invoice %s is overdue"
msgstr "La factura %s está pendiente"
#, php-format
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d archivo"
msgstr[1] "%d archivos"
Validating PO files
The GNU gettext tools catch most problems before they reach users. msgfmt --check validates the syntax and the header and, for entries flagged php-format or c-format, that the translation uses the same placeholders as the source. msgattrib --only-fuzzy lists the entries waiting for review, and msgmerge updates an existing PO file from a new POT template, keeping existing translations and marking near matches as fuzzy.
Two behaviours are worth knowing. msgfmt leaves fuzzy entries out of the compiled MO file unless you pass --use-fuzzy, so a fuzzy translation shows the source text at runtime. And the format checks only understand printf-style placeholders: a Laravel :name placeholder dropped by a translator passes msgfmt --check without a warning, so check those with your own script.
msgfmt --check --statistics -o es.mo es.po
# 3 translated messages, 1 fuzzy translation.
msgattrib --only-fuzzy es.po
msgmerge --update es.po messages.pot
PO files in a Laravel project
Laravel does not read PO files, but they map cleanly onto its JSON translations, because both use the source text as the key. Converting means taking the translated, non-fuzzy entries and writing each msgid and msgstr pair to lang/es.json.
Two kinds of entry need manual handling. Entries with msgctxt cannot go to JSON, since one JSON key holds one translation; map them to structured keys in PHP files instead. Plural entries become pipe-separated strings for trans_choice(), with %d replaced by :count. For common languages such as Russian, Polish and Arabic, Laravel's plural rules use the same form order as the standard gettext Plural-Forms expressions, but compare the header of each file with your target language before converting plurals automatically.
The script below handles the singular, context-free entries after msgattrib has removed untranslated and fuzzy ones.
// First: msgattrib --translated --no-fuzzy --no-wrap -o clean.po es.po
$po = file_get_contents('clean.po');
preg_match_all('/^(?:msgctxt "(.*)"\n)?msgid "(.+)"\nmsgstr "(.+)"$/m', $po, $matches, PREG_SET_ORDER);
$lines = [];
foreach ($matches as [, $context, $source, $translation]) {
if ($context !== '') {
continue; // JSON keys cannot carry msgctxt: map these to structured keys
}
$lines[stripcslashes($source)] = stripcslashes($translation);
}
file_put_contents('lang/es.json', json_encode($lines, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));