How to translate a WPML export (XLIFF files in a ZIP)

How to translate a WPML export (XLIFF files in a ZIP)

September 2, 2026

If you have just exported translation jobs from WPML and you are looking at a ZIP file wondering what to do with it, this is the guide. You do not need to unzip it, and you do not need to open a single XLIFF file.

What you are actually holding

WPML’s export is a ZIP archive with one folder inside it, called posts/, and one .xliff file per post. Nothing else.

The numbers below come from a real export of 44 posts, measured rather than estimated, so you can compare them with your own.

A WPML export is one ZIP holding one XLIFF file per post. This one carried 44 posts, 228 translatable units and four target languages.

Two things about that archive surprise most people.

Each file declares its own language pair. The <file> element at the top of every XLIFF says what it came from and what it is going to:

<file original="103-3aeb85935a8505760aa368dc691898a7"
      source-language="fr"
      target-language="en"
      datatype="plaintext">

So a single export can hold four different target languages at once, which the one in the example did: French into English, German, Spanish and Italian. The archive is not one translation job. It is four, interleaved.

WPML tells you the size of the job. Every file carries its own word count in a custom attribute:

tool:wpml-words-to-translate-count="843"

Add them up and you know exactly what you are dealing with before you commit to anything. In the example: 34,829 words.

What is inside one file

An XLIFF file is XML. Each translatable piece of text is a <trans-unit> with a <source> and a <target>, and the content is wrapped in CDATA so that HTML inside a post does not break the XML around it.

Here is one unit, complete. This one had already been through translation memory, which the state attributes tell you:

<trans-unit resname="title" restype="string" datatype="html" id="title">
  <source><![CDATA[Bien ventiler son logement en hiver]]></source>
  <target state="needs-review-translation" state-qualifier="tm-suggestion">
    <![CDATA[Ventilating your home in winter]]>
  </target>
  <tool:extradata unit="Title" type="text" group="Main Content" group_id="Main_Content-0"/>
</trans-unit>

Units in that state were the exception in the export: 18 of 228. The other 210 carry no state attribute at all, and their target holds the source text word for word.

And here is the detail that decides how you approach the whole job.

WPML copies the source into the target as a starting point, so a file full of targets can still be a file with nothing translated.

WPML writes the source text into the target as a starting point. So an XLIFF file where every <target> is populated is not a translated file. In the real export, 217 of the 228 units still held the source text word for word. Only 18 carried an actual suggestion from translation memory, marked state="needs-review-translation".

If you skim the file, you will conclude it is already done. It is not.

The units you will find

WPML does not only export the post body. In the example export, the 228 units broke down like this:

Unit id What it is Count
title the post title 40
body the post content, as HTML 36
URL the slug 32
t_N categories and tags 68
field-_yoast_wpseo_title Yoast SEO title 16
field-rank_math_title Rank Math SEO title 16
field-_yoast_wpseo_metadesc Yoast meta description 8
field-rank_math_description Rank Math meta description 8
package-string-N page builder strings 4

Every unit also carries a <tool:extradata> tag naming what it is and which group it belongs to, which is how you can tell a category apart from a title without guessing.

Notice the SEO plugin fields. Your meta titles and descriptions are in this archive, which is good news and a decision to make: a meta description translated literally is rarely the one that earns the click in the target language.

Notice URL too. Your slugs are translatable, and in the example export they had been deliberately left in the source language. That is a real choice with consequences for your existing links, and only you can make it.

The shortcodes are already declared

This is the nicest thing WPML does and almost nobody notices it. The header of each file contains a phase listing every shortcode registered on your site:

<phase phase-name="shortcodes" process-name="Shortcodes identification">
  <note>wp_caption,caption,gallery,playlist,audio,video,embed,
  wpml_language_switcher,product_brand,...</note>
</phase>

Your custom shortcodes are in there alongside the WordPress ones. It is a machine-readable list of things that must survive the round trip untouched.

Why the obvious routes hurt

The same 44 files, handled one at a time or sent as a single job.

Opening them one at a time means finding the target lines inside XML, translating around HTML tags and shortcodes, saving, and doing it 43 more times. Then again for each of the other three languages.

Sending them to a translator or an agency means the language work is fine and the file work is not. You are paying an expert hourly rate for someone to be careful with XML, and you will pay it again on the next content update. The coordination is the cost, not the per-word rate.

Pasting the body into a chat assistant works for one post. By the tenth you have lost track of which files are done, the terminology has drifted, and nothing carries your decisions from one file to the next.

The whole round trip, in five steps

The whole round trip. Everything before step four is free, so a wrong plan costs nothing to correct.

Everything before step four is free. That matters more than it sounds: a plan that misread your archive costs nothing to correct, so there is no reason to guess.

Two ways to run the middle

Steps two to four can happen in a browser or entirely from your terminal. The choice changes who does the typing, not what comes back.

Two ways to run the same job. Pick either one, they converge on the same archive.

Pick by how you already work. If you are reading this with a terminal open, option B is fewer steps. If you are not, option A is not a lesser version of it.

Route 1: send the ZIP as it is

AI Glot accepts both XLIFF and ZIP, so a WPML export goes in unopened. Upload the archive, then describe the job in one sentence.

The instruction is free text, and this is where the export’s own quirks become easy instead of annoying:

Translate every trans-unit into the target language declared in each file.
Only translate units where the target still holds the same text as the source.
Leave the URL units exactly as they are.
Keep every shortcode, HTML tag and CDATA wrapper untouched.

Read that second line again, because it is the one that matters. “Only translate units where the target still holds the same text as the source” is exactly the 217-of-228 situation described above, and it means you do not pay to redo the 18 units your translation memory had already handled.

You get a plan back before anything is spent: the languages found, the scope, the word count and the cost. If the plan misread something, correct it in a sentence and it plans again. Then you download one ZIP with the same structure, ready to import.

Route 2: let your AI agent do it

If you work with Claude, ChatGPT, Cursor or any other coding agent, you do not have to touch a browser at all. Our command line tool signs in with OAuth and takes a file directly, so an agent can run the whole job.

Install and sign in once:

npm install -g @ai-glot/cli
aiglot auth login

That opens a browser to authorize the machine. On a server with no browser, use aiglot auth login --device for the device-code flow, or aiglot auth login --key <key> in CI.

Then hand your agent the location of the file and let it work. Something like this is enough:

My WPML export is at ~/Downloads/my-site-export.zip. It is a ZIP of XLIFF
files, one per WordPress post, and each file declares its own target
language.

Use the aiglot CLI to translate it. Run `aiglot batches create --help` and
`aiglot batches approve --help` first so you use the real flags. Create the
batch from the ZIP with an instruction that translates only the units whose
target still equals the source, and leaves the URL units alone. Show me the
plan and the cost before you approve anything. Use the lite quality tier.
Then poll until it is done and download the result next to the original.

Two things make that prompt work, and both are worth copying.

It tells the agent to read the help first. An agent that guesses flag names will fail on the first call and then invent a reason. aiglot help --json dumps every command, flag and exit code in one go if it prefers structure.

It puts a stop before the spend. aiglot batches approve is the only command that costs credits. Everything before it is free, so asking to see the plan first costs nothing and prevents the expensive kind of misunderstanding.

For reference, this is the sequence the agent will end up running:

# 1. Create the batch. Free. Returns a plan and an id.
aiglot batches create ~/Downloads/my-site-export.zip \
  --instruction "Translate every trans-unit into the target language declared in each file. Only translate units whose target still holds the same text as the source. Leave URL units unchanged. Keep shortcodes, HTML and CDATA intact." \
  --json

# 2. Approve. This is the only step that spends credits.
aiglot batches approve <batch-id> --quality lite

# 3. Poll until it finishes.
aiglot batches get <batch-id> --json

# 4. Download the translated archive.
aiglot batches download <batch-id> --output ~/Downloads/my-site-export.translated.zip

There is also an MCP server (aiglot mcp) if your agent prefers tools over a shell. The CLI is the better starting point: it works in a script, it works in CI, and you can see exactly what ran.

Getting your glossary involved

If your site has terms that must not drift, a product name, a house translation of a recurring phrase, put them in a workspace glossary once. They then apply to every file in the archive and to the next export too.

This is the part a machine translator handles poorly. Its glossary substitutes a term wherever it appears. A glossary here is treated as a lemma, so it inflects and conjugates into the sentence around it instead of dropping a fixed string into the middle of it.

Importing back into WPML

The translated archive comes back with the same structure: the same posts/ folder, the same filenames, the same units in the same order, with the targets filled in. WPML’s translation jobs screen takes it back the same way it gave it to you.

The same unit before and after. Only the target line changed, which is the whole promise of a round trip.

Two suggestions from the shape of the file itself. Import one file first and look at the post in WordPress before you commit the whole archive, because a page builder string in the wrong place is much easier to see rendered than in XML. And decide about the slugs before you import, not after: changing a published URL is a redirect you will have to maintain.

The honest scope of this

Use this for the bulk. Forty-four posts across four languages is exactly the job that is miserable by hand and unremarkable in one pass. Then put human review where it earns its keep: your highest-traffic pages, and anything where the wording is the product. Plenty of teams run AI Glot as a first pass and have a reviewer go over the output, which is a good use of an expert and a bad use of an afternoon spent inside XML.

For a homepage, hire a translator. For the archive of 44 posts you exported this morning, do not spend your week on it.


The export structure described here was measured from a real WPML export of 44 posts across four language pairs. Your export may differ if your site uses a different page builder or SEO plugin, and the unit ids will reflect your own plugins. The posts/ folder, the XLIFF 1.2 format, the per-file language pair and the source-copied targets are the parts that were consistent across all 44 files.

10,000 words free when you sign up

Ready to translate your large files?