Skip to content

Locale Sync: formattedText translations render as literal HTML after XLIFF import #631

@reichhartd

Description

@reichhartd

Summary

When importing an XLIFF file via the Locale Sync plugin, translations for formattedText elements display raw HTML tags as visible text on the page (e.g. <p dir="auto">Datenschutzerklärung</p> instead of just Datenschutzerklärung). This only affects non-default locales — the default locale renders correctly.

Steps to Reproduce

  1. Create a Framer project with a non-default locale (e.g. German)
  2. Add some formattedText content (headings, paragraphs)
  3. Use "Translate All" to generate translations — they display correctly
  4. Export the XLIFF using Locale Sync
  5. Edit a few translations in the exported XLIFF file (change the text inside <target> elements)
  6. Re-import the edited XLIFF via Locale Sync
  7. Preview the non-default locale

Expected: Translations display as normal text.
Actual: Translations show literal <p dir="auto">...</p> tags as visible text on the page.

Screenshots

Before import (correct):

Datenschutzerklärung

After import (broken):

<p dir="auto">Datenschutzerklärung</p>

Root Cause

The bug is in xliff.ts, specifically in createValuesBySourceFromXliff().

On import, the plugin reads the target value using:

const targetValue = target.textContent

textContent strips all HTML markup from the parsed XML node. For formattedText sources, the XLIFF target contains XML-escaped HTML like &lt;p dir=&quot;auto&quot;&gt;Text&lt;/p&gt;. The XML parser decodes the entities and creates actual DOM elements. textContent then returns only the text content without the HTML structure.

When this plain text is passed to framer.setLocalizationData(), Framer receives a plain string for a formattedText slot. It wraps it in <p dir="auto"> internally, but as a literal string rather than parsed HTML — so the tags render visibly.

This also means that any rich text formatting in translations (bold, links, line breaks) is silently stripped on every import, even if the XLIFF file contains the correct HTML structure.

Suggested Fix

The import function should differentiate between formattedText and other source types. For formattedText, it should preserve the HTML structure:

// In createValuesBySourceFromXliff(), around line 151:

const type = unit.querySelector('note[category="type"]')?.textContent

let targetValue: string
if (type === 'formattedText') {
    // Preserve HTML structure for formatted text
    targetValue = target.innerHTML
} else {
    targetValue = target.textContent ?? ""
}

This ensures that formattedText translations retain their <p>, <a>, <strong>, <br> etc. tags, while string and other types continue to work as plain text.

Environment

  • Framer Web (latest)
  • Locale Sync plugin (latest from Marketplace)
  • XLIFF 2.0 format
  • Tested with German (de) as non-default locale, English as default

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions