PO4A.1P(1)

πŸ’… PO4A.1P(1) β€” the po4a glow-up era

okay so bestie, buckle up, this is PO4A.1P(1) from the User Contributed Perl Documentation section, and it's genuinely one of the most useful little gremlins in the docs toolchain. no cap. let's get into it πŸŽ€

✨ NAME ✨

po4a β€” updates both the PO files and the translated documents in one shot. one command, both jobs, iconic behaviour honestly πŸ‘‘

✨ SYNOPSIS ✨

po4a [options] config_file

✨ DESCRIPTION ✨

so po4a (which stands for PO for anything, yes really, that's the name, we love a pun) makes it so much easier to keep documentation translations alive, and it does it using the classic gettext tools you already know. the main character feature here is that po4a decouples the translation of content from its document structure β€” the words and the layout are living separate lives and honestly? good for them. if you want a gentle intro to the whole project, go read the po4a(7) page, it's giving tutorial energy πŸ¦‹

when it runs, po4a parses every documentation file listed in its configuration file. then it:

  • updates the PO files (those are the ones holding the actual translations) so they reflect any change made to the documentation, and
  • produces a translated documentation by injecting the translated content (pulled straight out of the PO files) into the structure of the original master document.

so at first, the PO files only contain the strings that need translating from the original docs. the PO format is what lets translators hand-craft a translation for every paragraph po4a extracted. and here's the clever bit β€” if the documentation gets modified after it's been translated, po4a marks the affected translations as "fuzzy" in the PO file, which is basically a little flag going "hey bestie, a human needs to look at this one" πŸ™ˆ. translators can also drop in what's called an "addendum" β€” extra content that says stuff like who did the translation and how to report bugs. cute, useful, we stan.

        master documents ---+---->-------->---------+
         (doc authoring)    |                       |
                            V   (po4a executions)   >-----+--> translated
                            |                       |     |     documents
        existing PO files -->--> updated PO files >-+     |
             ^                            |               |
             |                            V               |
             +----------<---------<-------+               ^
              (manual translation process)                |
                                                          |
        addendum -->--------------------------------------+

the whole workflow is asynchronous, which is exactly what open-source projects need. the doc writers author the master documents at their own pace (no pressure, no deadlines, we love that for them ✨). the translators review and update the translations inside the PO files. then the maintainers rerun po4a whenever needed, to push any change in the original documentation through to the PO files, and to produce fresh documentation translations by injecting the latest translation into the latest document structure. everybody's working on their own timeline and it just works 🫢

🌸 the 80% rule (yes it's a thing)

by default, a translated document only gets produced when at least 80% of its content is translated. anything untranslated just stays in the original language. so yeah, the resulting documentation mixes languages if the translation isn't finished. you can move that 80% threshold around with the --keep option (described below, keep reading πŸ’–).

ngl, there's a real tension here: throwing away translations the moment they're not 100% done can be super discouraging for translators, because their work would almost never actually be shown to users. but on the flip side, showing "translations" that are way too incomplete can be genuinely confusing for the end users. pick your struggle πŸ’€

πŸŽ€ version control tea

storing the translated documentation files in your version control system is probably a bad idea, because they're automatically generated. the precious files are the PO files β€” those contain the actual hard work of your fellow translators, protect them with your life πŸ‘‘. also, some people find it easier to interact with translators through an online platform such as weblate, but that's naturally fully optional, do whatever vibes with your workflow.


πŸ¦‹ Quick start tutorial

okay so let's say you maintain a program named foo, and it has a man page at man/foo.1 written in English. (English is the bridge language in most open-source projects, but just so we're clear β€” po4a can be used from or to any language, it's not picky πŸ’…)

so some times ago, someone handed you a German translation named man/foo.de.1… and then vanished into the void. classic. and that's a problem, because you just got a bug report saying your documentation contains a gravely misleading information that absolutely must be fixed in all languages. but you don't speak German 😭 so you can only modify the original, not the translation. and now another contributor shows up wanting to contribute a Japanese translation β€” a language you don't master either. the universe is testing you fr fr.

it is time to convert your documentation to po4a and solve your documentation maintenance nightmares once and for all. you want to update the docs when needed, you want to make your fellow translators' lives easier, and you want to make sure your users never ever see outdated (and therefore misleading) documentation. periodt.

the conversion has two steps: set up the po4a infrastructure, and convert that previous German translation so you can salvage the earlier work. that second part is done with po4a-gettextize, like this:

po4a-gettextize --format man --master foo.1 --localized foo.de.1 --po de.po

and as the docs for po4a-gettextize(1) explain in detail, this process is rarely fully automatic β€” but once it's done, that de.po file holding the German translation can be integrated straight into your po4a workflow. slay πŸŽ€

πŸ’œ configuring po4a

now let's configure po4a itself. with the right file layout, your configuration file can be as simple as this:

[po_directory] man/po4a/

[type: man] man/foo.1 $lang:man/translated/foo.$lang.1

what this says is: all the PO files (the ones holding the translators' work) live in the man/po4a/ directory, and you've got exactly one master file, man/foo.1. if you had several master files, you'd just have several lines similar to that second one. each of those lines also tells po4a where to write the corresponding translation file. in this setup, the German translation of man/foo.1 lands at man/translated/foo.de.1. πŸ¦‹

⭐ the POT file moment

the last thing needed to complete the po4a configuration is a POT file β€” that's the template material used to start a new translation. just create an empty file with the .pot extension inside the specified po_directory (so, for example, man/po4a/foo.pot), and po4a will fill it with the expected content for you. no manual labour required, we love a self-filling template 🫢

here's a recap of all the files in this setup:

β”œβ”€β”€ man/
β”‚   β”œβ”€β”€ foo.1        <- The original man page, in English
β”‚   β”œβ”€β”€ po4a/
β”‚   β”‚   β”œβ”€β”€ de.po    <- The German PO translation, from gettextization
β”‚   β”‚   └── foo.pot  <- The POT template of future translations (empty at first)
β”‚   └── translated/  <- Directory where the translations will be created
└── po4a.cfg         <- The configuration file

πŸ”₯ running it

once that's all set up, running po4a will parse your documentation, update the POT template file, use that to update the PO translation files, and then use those to update the documentation translation files. all in one single command:

po4a --verbose po4a.cfg

and that's literally it, po4a is now fully configured πŸ’– once you've fixed your error in man/foo.1, the offending paragraph in the German translation gets replaced by the fixed text in English. mixing languages isn't optimal, sure, but it's the only way to remove errors from translations you don't even understand, and to make sure the content your users see is never misleading. and updating the German translation is so much easier in the corresponding PO file anyway, so that language mix-up probably won't last long. πŸ™ˆ

finally β€” when a Japanese translator wants to contribute a new translation, she should rename that foo.pot into ja.po and complete the translation. once you have that file, just drop it in man/po4a/po/. a translated page will appear as man/translated/foo.ja.1 (provided enough content is translated, remember the 80% rule bestie πŸ’€) the next time you run po4a again. iconic behaviour all round πŸ‘‘πŸŒ·

✨ OPTIONS ✨

  • -k, --keep β€” the bare minimum translation percentage you need to hit for po4a to actually keep (i.e. write) the resulting file. Default is 80, so out of the box a file has to be at least 80% translated before it lands on disk. No cap, that's just the vibe threshold πŸ’…
  • -w, --width β€” the column where we should wrap the resulting file, if the format even supports wrapping. Default is 76. If you hand it a value of 0 or anything negative, the resulting file won't get wrapped at all β€” which is genuinely useful with some formats that break when they get rewrapped 😭
  • -h, --help β€” shows a short help message. That's it, that's the whole thing 🫢
  • -M, --master-charset β€” the charset of the files containing the documents you want to translate. Heads up: all master documents have to use the same charset, no mixing πŸŽ€
  • -L, --localized-charset β€” the charset of the files holding the localized documents. Same rule applies: all translated documents will use the same charset.
  • -A, --addendum-charset β€” the charset of the addenda. And yeah, all the addenda should be in the same charset too. Consistency is the whole game ✨
  • -V, --version β€” displays the version of the script and exits. Iconic behavior honestly.
  • -v, --verbose β€” increases the verbosity of the program. More words, more info, more you knowing what's going on πŸ”₯
  • -q, --quiet β€” decreases the verbosity of the program. Shhh 🀫
  • -d, --debug β€” outputs some debugging information. For when you need to see behind the curtain πŸ‘€
  • -o, --option β€” extra option(s) to pass to the format plugin. Check the docs of each individual plugin for the valid options and what they actually mean. Example time: you could pass -o tablecells to the AsciiDoc parser, while the text parser would accept -o tabs=split. Different plugins, different vibes πŸ’œ
  • -f, --force β€” always generate the POT and PO files, even when po4a is like "girl we don't need to".

The default behavior (when --force is not specified) goes like this:

  • If the POT file already exists, it gets regenerated if a master document or the configuration file is more recent (unless --no-update is provided). The POT file is also written in a temporary document and po4a verifies that the changes are actually, genuinely needed.
  • Also, a translation is regenerated only if its master document, the PO file, one of its addenda, or the configuration file is more recent. And to avoid trying to regenerate translations that don't pass the threshold test (see --keep), a file with the .po4a-stamp extension can be created (see --stamp).

If a master document includes files, you should really use the --force flag, because the modification time of those included files is not taken into account. Not me warning you about a footgun 😭

Oh and the PO files are always re-generated based on the POT with msgmerge -U.

  • --stamp β€” tells po4a to create stamp files when a translation isn't generated because it doesn't reach the threshold. These stamp files are named according to the expected translated document, with the .po4a-stamp extension.
Note: this only activates the creation of the .po4a-stamp files. The stamp files are always used if they exist, and they get removed with --rm-translations or when the file is finally translated. iykyk πŸ¦‹
  • --no-translations β€” don't generate the translated documents, only update the POT and PO files.
  • --no-update β€” don't change the POT and PO files, only the translation may be updated.
  • --keep-translations β€” keeps the existing translation files even if the translation doesn't meet the threshold specified by --keep. This option does not create new translation files with little content, but it will save existing translations that decay because of changes to the master files.
WARNING: this flag changes the po4a behavior in a rather drastic way: your translated files will not get updated at all until the translation improves. Only use this flag if you prefer shipping an outdated translated documentation rather than only shipping an accurate untranslated documentation. Like, that's the actual trade-off, no sugarcoating it πŸ’€
  • --rm-translations β€” removes the translated files (and implies --no-translations).
  • --no-backups β€” this flag does literally nothing since 0.41, and might get removed in later releases. Zombie option fr fr 🧟
  • --rm-backups β€” same situation: does nothing since 0.41, may be removed in later releases.
  • --translate-only translated-file β€” translate only the specified file. Genuinely useful for speeding up processing if a configuration file contains a lot of files. Just note that this option does not update PO and POT files. And yes, you can use this option multiple times πŸ’«
  • --variable var=value β€” define a variable that will be expanded in the po4a configuration file. Every occurrence of $(var) gets replaced by value. Can also be used multiple times, go off πŸ‘‘
  • --target-lang LANG β€” update PO files and generate translated documents only for the specified language. Multiple uses allowed.
  • --srcdir SRCDIR β€” set the base directory for all input documents specified in the po4a configuration file.

If you specify both destdir and srcdir, input files are searched in the following directories, in order: destdir, the current directory, and srcdir. Output files are written to destdir if specified, or to the current directory.

  • --destdir DESTDIR β€” set the base directory for all the output documents specified in the po4a configuration file (see --srcdir above).

πŸŽ€ Options modifying the POT header πŸŽ€

  • --porefs type β€” specify the reference format. The type argument can be one of: never to not produce any reference at all, file to only specify the file without the line number, counter to replace the line number with an increasing counter, and full to include complete references (default: full).
  • --wrap-po no|newlines|number (default: 76) β€” specify how the po file should be wrapped. This is the choice between files that are nicely wrapped but could lead to git conflicts, or files that are easier to handle automatically but harder to read for humans. A classic dilemma, honestly 😭

Historically, the gettext suite has reformatted the po files at the 77th column for cosmetics. This option specifies the behavior of po4a. If set to a numerical value, po4a will wrap the po file after this column and after newlines in the content. If set to newlines, po4a will only split the msgid and msgstr after newlines in the content. If set to no, po4a will not wrap the po file at all. The reference comments are always wrapped by the gettext tools that we use internally.

Note that this option has no impact on how the msgid and msgstr are wrapped, i.e. on how newlines get added to the content of these strings. Different thing entirely, bestie πŸ’…
  • --master-language β€” language of the source files containing the documents to translate. Note that all master documents must use the same language.
  • --msgid-bugs-address email@address β€” set the report address for msgid bugs. By default, the created POT files have no Report-Msgid-Bugs-To fields.
  • --copyright-holder string β€” set the copyright holder in the POT header. Default value is "Free Software Foundation, Inc."
  • --package-name string β€” set the package name for the POT header. Default is "PACKAGE".
  • --package-version string β€” set the package version for the POT header. Default is "VERSION".

πŸ¦‹ Options to modify the PO files πŸ¦‹

  • --msgmerge-opt options β€” extra options for msgmerge(1).
Note: $lang will be extended to the current language. Cute little shortcut ✨
  • --no-previous β€” this option removes --previous from the options passed to msgmerge. It's necessary to support versions of gettext earlier than 0.16. Throwback era πŸ•°οΈ
  • --previous β€” this option adds --previous to the options passed to msgmerge. It requires gettext 0.16 or later, and it's activated by default.

🌸 CONFIGURATION FILE 🌸

po4a expects a configuration file as its argument. This file must contain the following elements:

  • The path to the PO files and the list of languages existing in the project;
  • Optionally, some global options and so-called configuration aliases that are used as templates to configure individual master files;
  • The list of each master file to translate, along with specific parameters.

All lines contain a command between square braces, followed by its parameters. Comments begin with the char # and run until the end of the line. You can escape the end of line to spread a command over several lines. Handy 🫢

Some full examples are presented on this page, while other examples can be found in the t/cfg directory of the source distribution.

⭐ Finding the PO and POT files ⭐

The simplest solution is to explicitly give the path to POT and PO files, like this:

 [po4a_paths] man/po/project.pot de:man/po/de.po fr:man/po/fr.po

That specifies the path to the POT file first, and then the paths to the German and French PO files. Simple, explicit, slay.

The same information can be written like this to reduce the risk of copy/paste errors:

 [po4a_langs] fr de
 [po4a_paths] man/po/project.pot $lang:man/po/$lang.po

The $lang component is automatically expanded using the provided languages list, which reduces the risk of copy/paste error when a new language gets added. Literally the best feeling when you add a language and nothing breaks πŸ’–

You can compact the same information even further by only providing the path to the directory containing your translation project, like so:

 [po_directory] man/po/

The provided directory must contain a set of PO files, each named XX.po with "XX" the ISO 639-1 of the language used in this file. The directory must also contain a single POT file, with the .pot file extension. For the first run, this file can be empty but it must exist (po4a cannot guess the name to use before the extension). Like, it needs something to work with, bestie πŸ™ˆ

Note that you must choose only one between "po_directory" and "po4a_paths". The first one ("po_directory") is more compact, further reduces the risk of copy/paste error, but forces you to use the expected project structure and file names. The second one ("po4a_paths") is more explicit, probably more readable, and advised when you setup your first project with po4a. Start with the explicit one, trust me on this one πŸŽ€

πŸ’œ Centralized or split PO files? πŸ’œ

By default, po4a produces one single PO file per target language, containing the whole content of your translation project. As your project grows, the size of these files may become problematic. When using weblate, it's possible to specify priorities for each translation segment (i.e., msgid) so that the important ones get translated first. Still, some translation teams prefer to split the content in several files.

To have one PO file per master file, you simply have to use the string $master in the name of your PO files on the "[po4a_paths]" line, like this:

 [po4a_paths] doc/$master/$master.pot $lang:doc/$master/$lang.po

okok so with this line, po4a is gonna produce separate POT and PO files for each document you're translating. Like, example: 3 documents βœ• 5 languages = 3 POT files and 15 PO files, no cap. These files get named exactly as you specified on the po4a_paths template, with $master swapped out for the basename of each document to translate. And if the names collide? πŸ’€ Then you can name the POT file to use, like this, with the pot= parameter.

This feature also lets you group several translated files into the same POT file, which is lowkey iconic. The following example only produces 2 POT files: l10n/po/foo.pot (containing the material from foo/gui.xml) and l10n/po/bar.pot (containing the material from both bar/gui.xml and bar/cli.xml).

 [po4a_langs] de fr ja
 [po4a_paths] l10n/po/$master.pot $lang:l10n/po/$master.$lang.po
 [type: xml] foo/gui.xml $lang:foo/gui.$lang.xml pot=foo
 [type: xml] bar/gui.xml $lang:bar/gui.$lang.xml pot=bar
 [type: xml] bar/cli.xml $lang:bar/cli.$lang.xml pot=bar

In split mode, po4a builds a temporary compendium during the PO update so all the PO files can share translations with each other 🫢. And if two PO files have different translations for the same string? po4a marks that string as fuzzy and submits both translations in all the PO files containing that string. Then when the translator unfuzzies it, that translation is automatically used in every PO file. Slay.

Specifying the documents to translate πŸ’–

You also have to list the documents that should be translated, obviously. For each master file you gotta say: which format parser to use, where the translated document should go, and optionally some config. And bestie β€” quote or escape your file names if they contain spaces. Example:

 [type: sgml] "doc/my stuff.sgml"  "fr:doc/fr/mon truc.sgml"  de:doc/de/mein\ kram.sgml
 [type: man] script fr:doc/fr/script.1 de:doc/de/script.1
 [type: docbook] doc/script.xml fr:doc/fr/script.xml \
             de:doc/de/script.xml

But like… again, those complex lines are genuinely painful to read and modify (imagine adding a new language 😭). It's so much simpler to reorganize with the $lang template, like this:

 [type: sgml]    doc/my_stuff.sgml $lang:doc/$lang/my_stuff.sgml
 [type: man]     script.1          $lang:po/$lang/script.1
 [type: docbook] doc/script.xml    $lang:doc/$lang/script.xml

Specifying options ✨

There's two types of options, bestie, don't mix them up:

  • po4a options are default values for the po4a command line options.
  • format options are used to change the behavior of the format parsers.

As a po4a option you could, for example, say in your config file that the default value of the --keep command line parameter is 50% instead of 80%. Format options are documented on the specific page of each parsing module, e.g. Locale::Po4a::Xml(3pm). You could for example pass nostrip to the XML parser so it doesn't strip the spaces around the extracted strings.

You can pass these options for a specific master file, or even for a specific translation of that file, using opt: and opt_XX: for the XX language. In the following example, the nostrip option is passed to the XML parser (for all languages), while the threshold gets reduced to 0% for the French translation (which is thus always kept πŸ‘‘):

 [type:xml] toto.xml $lang:toto.$lang.xml opt:"-o nostrip" opt_fr:"--keep 0"

In any case, these configuration chunks must be at the end of the line. Order is: the declaration of files first, then the addendum if any (see below), and then only the options. The grouping of configuration chunks is not very important, since elements are internally concatenated as strings. All these examples are equivalent, fr fr:

  [type:xml] toto.xml $lang:toto.$lang.xml opt:"--keep 20" opt:"-o nostrip" opt_fr:"--keep 0"
  [type:xml] toto.xml $lang:toto.$lang.xml opt:"--keep 20 -o nostrip" opt_fr:"--keep 0"
  [type:xml] toto.xml $lang:toto.$lang.xml opt:--keep opt:20 opt:-o opt:nostrip opt_fr:--keep opt_fr:0
Heads up 🫢 β€” language specific options are not used when building the POT file. It's for example impossible to pass nostrip to the parser only when building the French translation, because the same POT file is used to update every language. So the only options that can be language-specific are the ones used when producing the translation, like the --keep option.

Configuration aliases πŸŽ€

To pass the same options to several files, the best move is to define a type alias, like this. In the next example, "--keep 0" is passed to every Italian translation using this test type, which is an extension of the man type:

  [po4a_alias:test] man opt_it:"--keep 0"
  [type: test] man/page.1 $lang:man/$lang/page.1

You can also extend an existing type by reusing the same name for the alias, like so. This is not interpreted as an erroneous recursive definition (it's giving self-love, not self-destruction πŸ’…):

  [po4a_alias:man] man opt_it:"--keep 0"
  [type: man] man/page.1 $lang:man/$lang/page.1

Global default options 🌈

You can also use [options] lines to define options that must be used for all files, regardless of their type:

 [options] --keep 20 --option nostrip

And just like with the command line options, you can abbreviate the parameters passed in the config file:

 [options] -k 20 -o nostrip

Option priorities 🧚

The options of every source are concatenated, making sure the default values can easily be overridden by more specific options. The order goes like this:

  1. [options] lines provide default values that can be overridden by any other source.
  2. Type aliases are then used. Language specific settings override the ones applicable to all languages.
  3. Settings that are specific to a given master file override both the default ones and the ones coming from the type alias. In this case also, language specific settings override the global ones.
  4. Finally, parameters provided on the po4a command line override any settings from the configuration file.

Example ⭐

Here's one showing how to quote the spaces and quotes:

 [po_directory] man/po/

 [options] --master-charset UTF-8

 [po4a_alias:man] man opt:"-o \"mdoc=NAME,SEE ALSO\""
 [type:man] t-05-config/test02_man.1 $lang:tmp/test02_man.$lang.1 \
            opt:"-k 75" opt_it:"-L UTF-8" opt_fr:--verbose

Addendum: Adding extra content in the translation πŸ¦‹

So you wanna add an extra section to the translation β€” like giving credit to the translator, which is honestly the sweetest thing πŸ₯Ή β€” then you need to define an addendum on the line defining your master file. Peep po4a(7) for the details on the syntax of addendum files.

 [type: pod] script fr:doc/fr/script.1 \
             add_fr:doc/l10n/script.fr.add

You can also use language templates like this:

 [type: pod] script $lang:doc/$lang/script.1 \
             add_$lang:doc/l10n/script.$lang.add
If an addendum fails to apply, the translation is discarded. Gone. Periodt. 😭

Modifiers for the addendum declaration πŸ’«

Addendum modifiers can simplify the config file when not all languages provide an addendum, or when the list of addenda changes from one language to the other. The modifier is a single char placed before the file name:

  • ? β€” include addendum_path if this file does exist, otherwise do nothing.
  • @ β€” addendum_path is not a regular addendum but a file containing a list of addenda, one by line. Each addendum may be preceded by modifiers.
  • ! β€” addendum_path is discarded, it is not loaded and will not be loaded by any further addendum specification.

The following includes an addendum in any language, but only if it exists. No error is reported if the addendum does not exist:

 [type: pod] script $lang:doc/$lang/script.1  add_$lang:?doc/l10n/script.$lang.add

And the following includes a list of addendum for every language:

 [type: pod] script $lang:doc/$lang/script.1  add_$lang:@doc/l10n/script.$lang.add

Filtering the translated strings 🌸

Sometimes you wanna hide some strings from the translation process. Same energy, honestly. To that end, you can give a pot_in parameter to your master file to specify the name of the file to use instead of the real master when building the POT file. Like this:

  [type:docbook] book.xml          \
          pot_in:book-filtered.xml \
          $lang:book.$lang.xml

With this setting, the strings to translate get extracted from book-filtered.xml (which must be produced before calling po4a) while the translated files are built from book.xml. So any string that's part of book.xml but not in book-filtered.xml will not be included in the PO files β€” preventing the translators from providing a translation for them. Meaning those strings get left unmodified when producing the translated documents. That naturally decreases the level of translation, so you may need the --keep option to ensure the document is produced anyway. Ngl, it's giving incomplete, but it's giving what you asked for πŸ’€


✨ SEE ALSO ✨

po4a-gettextize(1), po4a(7).

πŸ‘‘ AUTHORS πŸ‘‘

  • Denis Barbier <barbier@linuxfr.org>
  • Nicolas FranΓ§ois <nicolas.francois@centraliens.net>
  • Martin Quinson (mquinson#debian.org)

Copyright 2002-2023 by SPI, inc.

This program is free software; you may redistribute it and/or modify it under the terms of GPL v2.0 or later (see the COPYING file).


and that's the whole page, bestie!! 🫢 this edition is perl v5.42.0, dated 2025-07-13, and you just finished reading PO4A.1P(1) β€” go translate something beautiful, mwah πŸ’…πŸ’–

πŸ“¦ where this page comes from