Tagging Content

The connector works with Rosey's data-rosey attribute system. Any HTML element tagged with data-rosey becomes translatable — both in Rosey's build-time translation pipeline and in the connector's Visual Editor integration.

#Manual tagging

Add data-rosey to any element that contains translatable text. The attribute value is the translation key:

<h1 data-rosey="hero-title">Welcome to my site</h1>
<p data-rosey="hero-subtitle">The best site on the internet</p>

Choosing good keys

The key you put in data-rosey determines how translations are tracked across builds. There are three common strategies — pick the one that fits your workflow.

Use static, descriptive keys that don't change when the content changes. When the source text changes, the key stays stable and the connector flags the translation as stale rather than creating a new entry. This gives translators a clear signal that a specific translation needs updating, along with a diff of what changed.

<h1 data-rosey="homepage:hero-title">Welcome to Sendit</h1>
<p data-rosey="homepage:hero-subtitle">Email marketing made easy</p>

This is the recommended default for most sites. Keys are readable in the locale file, stale detection works fully, and keys survive content edits.

Content as the key

You can use the element's text content (or a slugified version) as the key. This was v1's default approach via generateRoseyId() and remains valid in v2.

<h1 data-rosey={heading.text.toLowerCase().replace(/\s+/g, "-")}>
  {heading.text}
</h1>
<!-- If heading.text is "Welcome to Sendit", key is "welcome-to-sendit" -->

The trade-off is that stale translation detection won't trigger. When the source text changes, the key changes too — the old key is orphaned (and cleaned up by the next write-locales run) and a brand-new entry appears with no translation. Since the new entry's original and _base_original are always identical, the connector never sees a mismatch to flag as stale.

However, this gives you a different kind of protection: changed content always shows up as untranslated, forcing a fresh translation. Nothing is silently outdated — it's just a clean-slate approach rather than a diff-based one.

You can safely leave stale detection enabled while using content-derived keys. Nothing breaks — _base_original is still written, it just never diverges from original for any living key. No false positives, no errors, no wasted work.

UUIDs as keys

UUIDs provide maximum key stability — content changes, reordering, and insertions never affect the key. Stale detection works perfectly and there's zero collision risk, which neatly solves the array/index problem.

The critical requirement is that UUIDs must come from stored data, not build-time generation. If you generate a UUID in your template (e.g. crypto.randomUUID()), every build produces new UUIDs and all translations are orphaned.

CloudCannon's instance_value feature is designed for exactly this. Configure a hidden text input with instance_value: UUID on your structure, and CloudCannon auto-populates a UUIDv4 when an array item is created. The UUID persists in the content file across rebuilds:

# cloudcannon.config.yml
_inputs:
  _uuid:
    type: text
    hidden: true
    instance_value: UUID

The best pattern is to use the UUID as a namespace segment (via data-rosey-ns) while keeping the leaf key descriptive. This way the key is stable AND the leaf tells you what the field is:

<div data-rosey-ns="features">
  {features.map((feature) => (
    <div data-rosey-ns={feature._uuid}>
      <h3 data-rosey="title">{feature.title}</h3>
      <p data-rosey="description">{feature.description}</p>
    </div>
  ))}
</div>
<!-- key: features:6ec0bd7f-11c0-43da-...:title -->

The trade-off is readability — locale files contain opaque UUIDs, so debugging requires cross-referencing with the source content. Using UUID-as-namespace with descriptive leaf keys mitigates this (you can see the field name, just not which array item).

If an item is deleted and re-added in CloudCannon, it gets a new UUID and the old translation is orphaned — a fresh translation is needed for the new item.

UUIDs shine for dynamic, CMS-managed arrays. For hand-authored templates without CMS-managed data, static descriptive keys are simpler and equally stable.

For a working example of UUIDs on content blocks alongside content-as-key on nav/footer links, see the Rosey Astro Starter.

Summary

Strategy Stale detection Readability Best for
Static descriptive Full High Most sites, hand-authored templates
Content as key None (forces re-translation instead) High Simple sites, short stable text
UUID (via instance_value) Full Low (mitigated with descriptive leaves) CMS-managed arrays and structures

All three are valid — the connector and Rosey use whatever keys the HTML provides. Choose based on your content management workflow.

Key uniqueness and stability

Each Rosey key maps to exactly one entry in the locale file. The connector relies on this 1:1 mapping for two things:

Both behaviours break when keys collide or shift unexpectedly.

The array/index problem. A common pitfall is using positional indexes as namespace segments for repeating structures (e.g. a list of feature cards):

<!-- Fragile: index-based keys shift when items are inserted or reordered -->
<div data-rosey-ns="features">
  <div data-rosey-ns="0"><h3 data-rosey="title">Fast</h3></div>
  <div data-rosey-ns="1"><h3 data-rosey="title">Secure</h3></div>
</div>
<!-- keys: features:0:title, features:1:title -->

If a new card is inserted between the two, every key after the insertion point shifts by one. The locale file still has entries for the old indexes, so:

Recommended: use stable identifiers. Instead of indexes, derive namespace segments from something that won't change when items are reordered — a slug, a short descriptive name, or a UUID from your CMS data:

<!-- Stable: content-derived keys survive insertions and reordering -->
<div data-rosey-ns="features">
  <div data-rosey-ns="fast"><h3 data-rosey="title">Fast</h3></div>
  <div data-rosey-ns="secure"><h3 data-rosey="title">Secure</h3></div>
</div>
<!-- keys: features:fast:title, features:secure:title -->

Now inserting a card between them doesn't affect existing keys — the new card simply gets a new key, and existing translations stay matched to the correct elements.

Note: Key design is ultimately a decision for the site author. The connector and Rosey use whatever keys the HTML provides — they don't enforce a naming strategy. Choose an approach that keeps keys unique and stable across content changes.

Elements with no locale entry yet

When switching to a locale in the Visual Editor, a [data-rosey] element whose resolved key has no matching entry in the locale file is still fully editable. It displays the source text as a fallback, and the first edit creates a new entry in the locale file for that key. The connector writes { original, value, _base_original }, where original and _base_original are seeded from the source text on the page and value is what you type. No build is required first.

This means you can translate newly added content directly in the Visual Editor before write-locales has run for it. Stale detection also works immediately: because _base_original is seeded from the current source text, the entry flips to stale the moment the source text later changes (the next write-locales run updates _base_original, creating the mismatch with original).

The entry the connector writes is the same shape write-locales produces, so a later build reconciles cleanly — existing keys keep their original and value, and only get _base_original refreshed.

A note on key stability. Because a missing key is now created silently on edit rather than flagged, an unstable key (see Key uniqueness and stability) no longer surfaces as an obvious "this element is disabled" signal — a shifted key just produces a new entry. Keep keys stable so edits land on the intended entry rather than scattering translations across orphaned keys.

Array items inside a component: keep data-rosey-ns live

When data-rosey-ns is derived from a per-item id (e.g. a _uuid from instance_value) and the array is rendered inside a component (a widget that holds its own list), the Visual Editor may add or reorder items by cloning a sibling's DOM without re-rendering — so a new item gets a stale, duplicated data-rosey-ns until you reload, which collides keys and breaks new-content translation. If you see this, give each array item its own registered component and put data-component on the array-item element, with data-rosey-ns on the item component's own root, so CloudCannon renders the item directly. See Astro patterns: Array items inside a component for a worked example. (SSG-specific: the same principle applies wherever an editable array lives inside a re-rendering component.)

Works with CloudCannon editable regions

If you're using CloudCannon's editable region custom elements, data-rosey can go right on them:

<editable-text data-editable="text" data-prop="title" data-rosey="hero:title">
  Welcome to my site
</editable-text>

Rich text regions: tag the region, not its contents

For a rich text region — data-editable="source", or a text region with data-type="text"/"block"data-rosey belongs on the region element itself, never on a block inside it:

<!-- Do this: the key is on the region element -->
<div
  data-editable="source"
  data-path="src/pages/about.astro"
  data-key="description"
  data-rosey="about:description">
  <p>We're a small team passionate about <strong>building great websites</strong>.</p>
</div>

<!-- Not this: the key is on markup CloudCannon owns -->
<div
  data-editable="source"
  data-path="src/pages/about.astro"
  data-key="description">
  <p data-rosey="about:description">We're a small team…</p>
</div>

CloudCannon parses a rich text region's contents into its editor schema and re-serializes them on save. The schema carries block structure and inline marks (<strong>, <a>) but not arbitrary attributes, so a data-rosey on a block inside the region can't be round-tripped — CloudCannon reports the element as uneditable rather than dropping the key. The region element is a different matter: it's the boundary rather than the content, so its own attributes are left alone. That's why a tag on the region element works even when the region is a semantic element carrying both attributes:

<h1 data-editable="source" data-path="src/pages/about.astro" data-key="heading" data-rosey="about:heading">
  About Us
</h1>

Key stability points the same way. The DOM inside a region is editor-owned — someone splitting a paragraph, adding a list, or reordering blocks changes which elements exist — so a key attached to one of those blocks isn't stable (see Key uniqueness and stability). data-key is stable, which makes the region the natural unit of translation: one editable region, one Rosey key.

Tagging the region means the translation value is the region's full inner HTML, block tags included. That's fine on both ends — Rosey replaces innerHTML, and the connector gives the locale editor an html input with the content toolbar, so a translator edits the block in a rich text editor that matches the original.

A snippet is not the fix. A _snippets entry can technically teach CloudCannon to round-trip <p data-rosey="…">, but it makes the Rosey key an editor-facing form field (duplicate the card and you silently collide keys) and replaces inline WYSIWYG prose with a snippet card. Keep Rosey keys in templates and component markup, out of editor-owned content.

Key namespacing

Rosey keys can be namespaced using parent element attributes. The connector walks up the DOM from each data-rosey element, collecting namespace segments to build a fully qualified key.

data-rosey-ns

Adds a namespace segment to all child keys:

<section data-rosey-ns="hero">
  <h1 data-rosey="title">Welcome</h1>
  <!-- Resolved key: hero:title -->
</section>

data-rosey-root

Sets the root prefix and stops further upward traversal:

<main data-rosey-root="index">
  <section data-rosey-ns="hero">
    <h1 data-rosey="title">Welcome</h1>
    <!-- Resolved key: index:hero:title -->
  </section>
</main>

Combined example

<body>
  <main data-rosey-root="index">
    <section data-rosey-ns="hero">
      <h1 data-rosey="title">Welcome</h1>
      <!-- key: index:hero:title -->
      <p data-rosey="subtitle">The best site</p>
      <!-- key: index:hero:subtitle -->
    </section>
    <section data-rosey-ns="features">
      <h2 data-rosey="heading">Features</h2>
      <!-- key: index:features:heading -->
    </section>
  </main>
</body>

The resulting locale file entries:

{
  "index:hero:title": { "original": "Welcome", "value": "..." },
  "index:hero:subtitle": { "original": "The best site", "value": "..." },
  "index:features:heading": { "original": "Features", "value": "..." }
}

Resetting the namespace

Setting data-rosey-root="" (empty string) resets the namespace. Child keys won't inherit anything from above:

<main data-rosey-root="index">
  <div data-rosey-root="">
    <p data-rosey="standalone">This key is just "standalone"</p>
  </div>
</main>

Shared content across pages

If the same text appears on multiple pages (e.g. navigation, footer), use consistent namespacing so the translations are shared. For example, give your header a namespace that resolves to the same key regardless of which page it's on:

<header data-rosey-root="common">
  <nav data-rosey-ns="nav">
    <a data-rosey="home" href="/">Home</a>
    <!-- key: common:nav:home — same on every page -->
  </nav>
</header>

Since the header lives outside the snapshot boundary, it won't be affected by locale switching in the Visual Editor. Rosey still translates it at build time.

Automatic tagging

v2 does not include an auto-tagger (the data-rosey-tagger attribute from v1 is not supported). If you previously relied on auto-tagging for markdown content rendered to HTML at build time, you'll need to add data-rosey attributes manually to your templates.

For content coming from markdown that you can't tag at the source level, consider:

See Migrating from v1 if you're upgrading from RCC v1 and used the auto-tagger.