All articles
10 min read

XLIFF Files Explained: What Developers Need to Know Before Sending Strings to Translators

What XLIFF is, how a 1.2 file is structured, the pitfalls that corrupt a translation round trip, and how to export and import it without losing data.

XLIFFLocalizationWorkflow

Ask a translation agency for a quote and the first question back is usually "can you send XLIFF?" Hand them a CSV export instead and you'll usually get a polite reply asking for XLIFF anyway. Most developers have never opened one of these files and have no idea why a three-letter acronym carries that much weight in an industry that otherwise runs on spreadsheets.

The short answer: XLIFF is not a translation format in the way JSON or YAML are. It is an exchange format built specifically for the tools translators use, and it carries information a plain key-value file cannot: per-segment status, inline markup that must survive untouched, and both languages in the same document. Once you've built one by hand, or torn one apart that came back wrong, the rest of the format makes sense.

What XLIFF actually is

XLIFF stands for XML Localization Interchange File Format. It's an OASIS standard, which means it isn't owned by any single vendor – a file produced by one tool is meant to open cleanly in another. That's the whole point of it existing: before XLIFF, every CAT (computer-assisted translation) tool had its own proprietary project format, and getting work out of one tool and into another meant a painful, lossy conversion.

There are two versions in circulation, 1.2 and 2.0. This piece sticks to 1.2, because it's the one still exchanged almost everywhere – translation agencies, freelance CAT tools and localization platforms overwhelmingly read and write it, and a file you hand someone today is far more likely to round-trip cleanly if it's 1.2.

The structural idea is simple once you see it: a file element names a source language and a target language, and inside it a list of trans-unit elements each hold one translatable segment, as a source/target pair. Unlike a JSON locale file, which only ever states where the translation ends up, an XLIFF file states where it came from as well. That's what makes it bilingual rather than single-language, and it's also what lets a translator work without needing to see your source codebase at all – the source text is sitting right next to the blank they need to fill in.

Anatomy of a 1.2 file

Here's a small, real XLIFF 1.2 document for two strings from a checkout flow:

<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
  <file original="checkout-app" source-language="en" target-language="de" datatype="plaintext">
    <body>
      <trans-unit id="checkout.button.submit" resname="checkout.button.submit">
        <source>Pay now</source>
        <target state="translated">Jetzt bezahlen</target>
        <note>Button label, imperative, max 18 characters</note>
      </trans-unit>
      <trans-unit id="checkout.summary.itemsInCart" resname="checkout.summary.itemsInCart">
        <source>Items in your cart: {count}</source>
        <target state="needs-review-translation"></target>
      </trans-unit>
    </body>
  </file>
</xliff>

Walking through the pieces:

  • file is the outer wrapper. source-language and target-language are attributes on it, not guesses – they apply to every trans-unit underneath. original is just a label for where the content came from; it has no functional meaning to most tools.
  • trans-unit is one translatable segment. id identifies it. Many tools additionally write resname, the "resource name" – a second identifier that's meant to survive even if a tool renumbers or regenerates the id values. If both are present, treat resname as the real key and id as disposable.
  • source holds the original text, in the file's declared source language. A translator sees this but normally doesn't edit it.
  • target holds the translation. An empty target means "not done yet" – that's a legitimate, common state, not malformed data.
  • note is free-text context for the translator – a character limit, a screenshot reference, a hint about tone. It's the one element a translator actually reads before they read anything else.
  • state (an attribute on target) is the segment's status in the CAT tool's own workflow.

The state attribute, briefly

state is worth understanding because it's the part developers skip past and then get confused by later. The common 1.2 values are new (nothing done), translated (a human or a machine filled it in), needs-review-translation (translated but not signed off), and final (reviewed and locked). A tool might also use needs-translation for a segment that was rejected and sent back. None of this is enforced by the XML schema – it's convention, and different CAT tools are not perfectly consistent about which values they emit – but as a reader, you can use state to tell "actually finished" apart from "has some text in it," which a bare JSON export can't express at all.

Where it goes wrong

Ids that don't survive a round trip

The id attribute looks like a stable key. It usually isn't. Plenty of CAT tools renumber id to their own internal sequence the moment they import a file, and if you re-export without resname, you get the translation back with no reliable way to match it to the key it came from. The fix is boring and effective: always write resname alongside id, and when you read a file back, prefer resname when both are present.

Inline markup

A string like Click <b>here</b> to continue doesn't travel through XLIFF as plain text. Real-world content, especially anything exported from a CMS or containing simple formatting, gets wrapped in XLIFF's own inline elements (g, x, bpt/ept, depending on the tool) so the translator can move the formatting without being able to break the markup itself. If your import treats a source or target body as plain text and doesn't expect inline elements, you'll either strip the formatting or import the raw tags as literal text. Keep a mental model that source/target content is "XML content," not "a string," even when ninety percent of the time it's a plain sentence with nothing in it.

Whitespace

XML preserves whitespace inside an element by default, which sounds helpful and then isn't. A trailing space in <source>Submit </source> is significant, and some CAT tools add or lose one on the way through, particularly across a save-reopen cycle in a desktop tool. A target that is otherwise a perfect translation but renders with a visible double space in a button is almost always this, not a translator's mistake.

Escaping

Standard XML escaping applies: &amp; for a literal ampersand, &lt; and &gt; for angle brackets, &quot; and &apos; for quotes where needed. The failure mode isn't exotic – it's a marketing string containing "Terms & Conditions" exported with a raw, unescaped ampersand, which breaks the XML parser for every tool downstream, not just the one that produced the bad file. If a file won't open at all, check for a bare & before you check anything else.

Duplicate ids and quietly dropped segments

Nothing in the format stops two trans-unit elements from sharing the same id – the schema doesn't enforce uniqueness, so a script that generates the file badly, or a merge of two partial exports done carelessly, can produce a file with a repeated key. Most parsers don't error on this. They just keep whichever one they saw last and silently drop the other, which means a translation can vanish from the output with no warning anywhere in the pipeline. If a string that was definitely translated isn't showing up after import, a duplicate id further up the file is worth checking before you assume the translator missed it.

Encoding mismatches

The XML declaration at the top of the file, <?xml version="1.0" encoding="UTF-8"?>, is a claim about the bytes that follow it, not an instruction that makes them true. Open an XLIFF file in a plain text editor, save it with the editor's default encoding, and it's easy to end up with a file that still declares UTF-8 while actually containing Windows-1252 bytes for an accented character. The result is a parser error on some tool and silent corruption of a handful of characters on another – café becomes caf├⌐ in the worst case. Treat XLIFF as a file you pass through tools, not one you hand-edit in a general-purpose editor, and this mostly goes away on its own.

The wrong language coming back

Because target-language is a declared attribute rather than something read off the filename, it's authoritative – which is a feature, right up until someone fills in the wrong value. A translator working from a template who forgets to update target-language from de to fr will hand back a file that says, in writing, "this is German," while the actual sentences are French. A tool that trusts the declared language (correctly, by the standard) will file the translation under German. There's no way to catch this except a human glancing at the actual text, which is a good argument for always spot-checking at least one segment of anything that comes back, especially from a new translator.

Sending a branch's strings out and getting them back

An XLIFF round trip through a branch

  1. Export the branch as XLIFFYou

    Pick the target language and the source language it should be paired with.

  2. Translator fills in the targetsTranslator or agency

    In their own translation tool. Source text, notes and state travel inside the file.

  3. Import the file backYou

    The file’s target-language decides where it lands, so de.xlf goes to German whatever the import screen says.

  4. ReviewDeveloper, admin or owner

    Approve or reject each string against its source, with a comment when it goes back.

  5. Merge to mainAdmin or owner

    Nothing the translator touched is live until this step.

This is where XLIFF earns its place in a normal release process rather than being a one-off export you generate by hand. Say your team has a feature-checkout-v2 branch with a dozen new or changed strings. You export that branch as XLIFF, pick a source language, hand the file to a translator or agency, and the file that comes back is the same shape with the blanks filled in.

In Translatize, exporting is where the "bilingual" part of XLIFF actually gets used: you choose a source language, the export pairs every string with its text in that language, and the target is left for the translator to fill. The export also takes care of most of the pitfalls above before the file leaves. Every trans-unit carries the key as resname as well as id, so a tool that renumbers ids can't orphan a translation. The key's description goes into note, so the context your team wrote is the first thing the translator reads. And state is set from each string's review status: approved strings go out as final, strings waiting for review as needs-review-translation, rejected ones as needs-translation, and empty ones as new – the translator can see at a glance what is settled and what needs work. Escaping is handled for you, so a "Terms & Conditions" string can't break the file.

Export from the app's export screen, or directly against the API with a project token:

curl -H "Authorization: Bearer $TRANSLATIZE_API_TOKEN" \
  "https://api.translatize.com/v1/integrations/projects/$PROJECT_ID/export?format=xliff&lang=de&sourceLang=en&branch=feature-checkout-v2" \
  -o feature-checkout-v2-de.xlf

When the translated file comes back, importing it is where the self-describing-language rule from earlier pays off directly: the file's own target-language wins over whatever language the import screen happens to have selected. A de.xlf lands as German translations no matter what, which matters a great deal once you're juggling files from three translators across five languages and can't personally verify every one before it's imported. Both languages populate on import too – the source text comes in alongside the target, so the platform's record of "what the translator was shown" matches what they actually saw, not just the final answer.

From there the normal review and branch workflow takes over: anyone on the project can submit an imported translation for review, a developer, admin or owner checks it against the source and approves or rejects it, and the branch merges back to main once an admin or owner runs the merge. The branching model itself is what keeps this isolated – the strings a translator is working on for feature-checkout-v2 don't touch what's already live, and nothing reaches production until the branch is merged.

If you're sending work to a human rather than running it through an AI translation pass, XLIFF is the format to reach for: it's the one format here that was built for exactly this handoff, rather than adapted to fit it. For anything running through a model instead of a person, the placeholder and plural handling described in translating JSON files with AI applies just as much to XLIFF's inline markup as it does to JSON's curly braces – the markup is the thing you have to protect, whichever format carries it.

Before you hand a file to a translator

A short list that has saved a round trip more than once:

  • Confirm source-language and target-language are both set, and correct, before the file leaves your hands.
  • Write resname on every trans-unit, not just id.
  • Don't hand-edit XLIFF in a text editor unless you're confident about escaping – a stray raw ampersand breaks the whole file for the next tool that opens it.
  • When a file comes back, spot-check at least one segment against the declared target language before you import it anywhere.
  • Keep notes (character limits, tone, context) in the note element rather than in a separate document a translator might not see.

None of this is complicated once you know to look for it. The format earns its reputation for being fussy mostly because people treat it like plain text and only find out otherwise when a file won't open.

Keep reading