Framer
Framer is a design and website builder where product copy often gets written first, either directly on the canvas or as entries inside a CMS collection. This guide shows how to extract that copy into a Messagevisor CSV so it flows through the same authoring, translation, and import path as the rest of your messages.
The translation step is covered by the other guides in this section (Google Sheets, DeepL, AI translations). This guide focuses on the upstream piece: turning Framer text into a CSV that Messagevisor's importer can read.
If you came here from the Figma guide, the shape is the same. The mechanics differ because Framer does not have a public REST file API. Extraction happens inside Framer, through a Framer plugin you run by hand or trigger from a button.
When to use this#
- Your team writes copy in Framer (either on the canvas or in CMS collections) before it lands in code.
- You want copywriters to keep working in Framer without having to learn Git.
- You want a repeatable extraction step instead of copy-pasting strings into YAML by hand.
- You build marketing pages, landing pages, or product surfaces in Framer and use Messagevisor for the app shell or backend-served copy.
If your copy lives in code first and Framer is only used for static visual design, you do not need this.
The end-to-end shape#
Framer project ─► plugin extracts text ─► Messagevisor CSV ─► import --createMissing ─► translate (DeepL / AI / translator) ─► import --applyThe new piece is the leftmost step. Everything to the right of the CSV is the same as any other workflow in this section.
Two source surfaces in Framer#
Framer copy lives in one of two places. Pick the path that matches where your team writes it.
Source A: text on the canvas#
Copy is written directly into text layers on Framer pages. Each text layer is a TextNode you can walk with the Framer Plugin API.
This is the simplest setup and the right starting point if your team uses Framer as a design tool first.
Source B: entries in a Framer CMS collection#
Copy lives as items inside a CMS collection (Buttons, Headings, MarketingCopy, etc.). Each item has fields, and one of the fields holds the user-facing string.
This is the cleaner setup if you want one canonical place per string, with a name and a description, and you want non-design contributors to edit copy without touching the canvas.
The plugin code below covers both. You can ship one plugin that handles both, or two smaller plugins, one per surface.
Prerequisites#
- A Framer project you have edit access to.
- Node.js and a recent npm available on your machine.
- The
framer-plugintooling installed locally to scaffold and run plugins:
$ npx framer-plugin@latest create messagevisor-extractor$ cd messagevisor-extractor$ npm installFramer's plugin tooling generates a small Vite + React scaffold. You will edit one file: src/App.tsx.
The Framer Plugin API surface evolves; method names and types in this guide reflect the public API at time of writing. When in doubt, check the Framer Plugin documentation for the current shape.
Step 1: Decide how Framer copy maps to message keys#
Just like the Figma guide, you need a naming convention so the plugin can turn each piece of Framer copy into a deterministic Messagevisor key.
Pattern A: name the canvas text layer after the key#
Rename text layers in Framer's layers panel to match the intended message key:
auth.signinauth.signoutdashboard.welcomemarketing.hero.headlineThe plugin uses the layer name verbatim as the message key. This is the most explicit pattern and what the example plugin below assumes.
Pattern B: use CMS field values#
In a CMS collection, add a key field next to the text field. The plugin reads item.key as the message key and item.text as the source-locale value:
| key | text | description |
|---|---|---|
marketing.hero.headline | Ship faster with Messagevisor | Hero headline on home page |
marketing.hero.subhead | The Git-based way to localize | Hero subhead on home page |
This pattern is sturdier than Pattern A because renaming the design layer does not break the key.
Step 2: Write the extractor plugin#
Open src/App.tsx in the scaffold and replace its contents with the extractor. The plugin shows two buttons (one per surface), generates a CSV, and downloads it.
import { framer, TextNode, CollectionItem } from "framer-plugin";import { useState } from "react";framer.showUI({ position: "top right", width: 280, height: 220 });export function App() { const [status, setStatus] = useState(""); async function extractFromCanvas() { setStatus("Reading canvas…"); const nodes = (await framer.getNodesWithType("TextNode")) as TextNode[]; const rows: Row[] = []; for (const node of nodes) { if (!looksLikeKey(node.name)) continue; const text = await node.getText(); rows.push({ key: node.name, description: node.name, value: text ?? "", }); } await downloadCsv("framer-canvas-en.csv", rows); setStatus(`Wrote ${rows.length} rows from the canvas.`); } async function extractFromCms() { setStatus("Reading CMS…"); const collections = await framer.getCollections(); const rows: Row[] = []; for (const collection of collections) { const items = (await collection.getItems()) as CollectionItem[]; for (const item of items) { const key = item.fieldData["key"]?.value as string | undefined; const text = item.fieldData["text"]?.value as string | undefined; if (!key || !looksLikeKey(key)) continue; rows.push({ key, description: collection.name, value: text ?? "", }); } } await downloadCsv("framer-cms-en.csv", rows); setStatus(`Wrote ${rows.length} rows from the CMS.`); } return ( <main> <button onClick={extractFromCanvas}>Extract canvas text</button> <button onClick={extractFromCms}>Extract CMS entries</button> <p>{status}</p> </main> );}type Row = { key: string; description: string; value: string };function looksLikeKey(name: string) { // Adjust to your project's namespace character. return /^[a-z0-9]+(\.[a-z0-9-]+)+$/.test(name);}async function downloadCsv(filename: string, rows: Row[]) { // Deduplicate; a string can appear on more than one Framer page. const seen = new Map<string, Row>(); for (const row of rows) { if (!seen.has(row.key)) seen.set(row.key, row); } const header = ["messageKey", "messageDescription", "en"]; const body = [...seen.values()].map((row) => [row.key, row.description, row.value]); const csv = stringifyCsv([header, ...body]); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url);}function stringifyCsv(rows: string[][]) { return rows.map((row) => row.map(csvCell).join(",")).join("\n") + "\n";}function csvCell(value: string) { const s = String(value ?? ""); return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;}Notes on the code:
framer.getNodesWithType("TextNode")walks every text layer in the project, not just the current selection. Filter to the selection withframer.getSelection()if you only want to extract what is highlighted.framer.getCollections()returns every CMS collection in the project. The plugin reads two fields by name (key,text). Change those to match your schema.- The
looksLikeKeyfilter prevents accidentally exporting designer scratchpad text. Keep the pattern tight; loose patterns are how garbage rows end up in your project. - Deduplication is done by
messageKey. The first occurrence wins. If two text layers have the same name but different copy, fix that in Framer before re-running. - The CSV is written client-side in the plugin via a
Blobdownload, not through a server. This keeps the plugin self-contained.
Step 3: Run the plugin#
From the scaffold directory:
$ npm run devThis starts the plugin in dev mode and prints a URL. Open your Framer project, choose Plugins ▸ Open Plugin from URL, and paste the URL.
The plugin window appears in Framer. Click Extract canvas text or Extract CMS entries. Framer prompts you to download the CSV. Save it to your Messagevisor project, for example exports/from-framer-en.csv.
For a one-shot extraction, dev mode is enough. If your team will run this repeatedly, publish the plugin to your Framer workspace through Framer's plugin publishing flow so it shows up alongside built-in plugins.
Step 4: Preview the Messagevisor import#
The CSV from Framer will usually introduce new message keys. Use --createMissing to allow that:
$ npx messagevisor import exports/from-framer-en.csv --createMissingRead the summary carefully. --createMissing is the flag most likely to introduce garbage if the plugin has a bug or a Framer layer name accidentally matches the key pattern.
Check:
- Is the count of
Created messagesclose to what you expected? - Are there
messageKeyvalues that look wrong (typos, casing, accidental dots)? - Are descriptions readable, or did the plugin emit raw layer names like
Frame 47?
Fix issues at the source (rename layers in Framer, tighten the looksLikeKey pattern, adjust CMS field names) and re-extract before applying.
Step 5: Apply the import#
When the preview looks right:
$ npx messagevisor import exports/from-framer-en.csv --createMissing --applyMessagevisor creates the new messages under messages/, with translations under the source locale (en in the example above).
Step 6: Translate into other locales#
Once the source-locale messages are in the project, translate them like any other batch of new strings:
- Google Sheets for a quick
GOOGLETRANSLATEpass. - DeepL for higher-quality machine translation via the API.
- AI translations for an agent-driven flow.
- Translator handoff for a human translator pass.
All four start with messagevisor export --onlyUntranslated, so they pick up exactly the keys you just imported from Framer.
Step 7: Validate and review#
$ npx messagevisor lint$ npx messagevisor test$ npx messagevisor catalogThe catalog shows the new messages immediately. This is often the first time anyone has seen the Framer copy outside Framer, and it surfaces things like wrong-context overrides, missing descriptions, and copy that does not fit its UI slot.
Tips and variations#
Run on selection only#
For large Framer projects, walking every text layer in the project on every run is slow. Add a third button to the plugin that uses framer.getSelection() and extracts only the selected layers. Designers can then highlight the section they just changed and export only that.
Round-trip translated copy back into Framer#
Extraction is one direction. If your Framer pages need to render translated copy back into the canvas (for example, to preview Dutch or German layouts in Framer), extend the plugin with a Pull translations button that reads a Messagevisor datafile and writes translated values back into the matching text layers.
The plugin would:
- Fetch the datafile for the desired locale (the same JSON your app reads at runtime).
- For each
TextNodewith a name matching a known key, callnode.setText(translation).
This turns Framer into a live preview surface for any locale you build, without an app deploy.
Encode override variants in layer names#
For copy that has an override (Pro plan vs free plan, web vs mobile), encode the override key in the layer name with the configured separator (default :):
pricing.cta (base translation)pricing.cta:pro (override for Pro plan)pricing.cta:mobile (override for mobile target)The plugin writes these as override rows and the Messagevisor importer handles them the same way as a CSV from a translator.
Use CMS for marketing pages, canvas for component copy#
A common split: marketing copy lives in a CMS collection (so a marketer can edit it without opening the canvas), and component copy (button labels, form errors) lives on the canvas next to the design. Run both extraction buttons and import both CSVs.
Pair with pseudo-localization#
Once the source copy is in Messagevisor, generate a pseudo-localized target locale (longer strings, accented characters) and pull it back into Framer using the Pull translations variation above. Layout problems in the design surface before any translator time is spent.
Compared to the Figma guide#
| Concern | Figma | Framer |
|---|---|---|
| Extraction surface | REST API (/v1/files/:key) | Plugin API running inside Framer |
| Token / auth | Personal access token | Plugin installed into the workspace |
| Source of copy | Text nodes only | Text nodes and CMS collections |
| Where extraction runs | A Node script on your machine or in CI | Inside Framer, triggered by a designer or developer |
| Best for | Design-first teams already using Figma | Teams that ship Framer-built sites or use CMS copy |
If your team uses both, run one extractor per tool and import both CSVs. The Messagevisor importer is happy to receive several CSVs from different sources as long as the messageKey values do not collide.

