Skip to main content

Convert XLIFF 2 to PO (gettext) — segment states, language metadata, and untranslated entries

Free command-line converter from XLIFF 2.0 back to gettext PO files. Preserves source, target, and untranslated entries with empty msgstr. Open source.

Free CLI — convert xliff2 files to po:

npm install -g @i18n-agent/i18n-convert
Need to translate, not just convert? Try i18nagent.ai MCP →

XLIFF 2 is the format translation vendors hand back; PO is the format your build pipeline actually compiles. The mismatch is structural rather than semantic: XLIFF 2 marks every segment with an explicit state attribute (initial, translated, reviewed, final) and groups them inside <unit> elements with stable ids, while PO has no first-class notion of translation state at all — an entry is "untranslated" simply because its msgstr is the empty string. Most XLIFF-to-PO converters either skip the untranslated units entirely or invent fake msgstr values from the source, both of which silently break downstream tooling that distinguishes "needs translation" from "translated to the same string as the source."

i18n-convert walks each XLIFF 2 unit, takes the <source> as the msgid, and emits the <target> as msgstr if it exists or an empty string if the segment state is initial. This is exactly the convention msgfmt and gettext-aware runtimes expect — an empty msgstr means "fall back to the source string at runtime." The fixture below is the canonical simple.xliff from the project's test suite: two translated units and one untranslated unit in state initial. Notice how "Not yet translated" round-trips with an empty msgstr, preserving the unfinished-work signal that an XLIFF reviewer originally set. The header msgid entry is emitted with empty fields because XLIFF 2 does not carry the PO header metadata.

Command

i18n-convert simple.xliff --to po -o messages.po

Input

<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
  <file id="f1" original="messages.json">
    <unit id="greeting">
      <segment state="translated">
        <source>Hello</source>
        <target>Hallo</target>
      </segment>
    </unit>
    <unit id="farewell">
      <segment state="translated">
        <source>Goodbye</source>
        <target>Auf Wiedersehen</target>
      </segment>
    </unit>
    <unit id="untranslated">
      <segment state="initial">
        <source>Not yet translated</source>
      </segment>
    </unit>
  </file>
</xliff>

Output

msgid ""
msgstr ""

msgid "Hello"
msgstr "Hallo"

msgid "Goodbye"
msgstr "Auf Wiedersehen"

msgid "Not yet translated"
msgstr ""

Related conversions