Figma
Designs are often where product copy is written first. Designers iterate in Figma, copywriters edit in Figma, and the engineering team only sees the strings when they show up in a pull request. This guide shows how to extract that copy from a Figma file into a Messagevisor CSV so it can flow through the same authoring, translation, and import path as the rest of your messages.
The translation step itself is covered by the other guides in this section (Google Sheets, DeepL, AI translations). This guide focuses on the upstream piece: turning Figma text nodes into a CSV that Messagevisor's importer can read.
When to use this#
- Your team writes UI copy in Figma before code is written.
- You want to seed
messages/files from a design instead of typing each string by hand. - You want a repeatable script you can re-run when the design changes, instead of copy-pasting strings one at a time.
- You want copywriters to keep working in Figma without having to learn Git.
If your team writes copy directly in YAML or in a CMS, you do not need this guide.
The end-to-end shape#
Figma file ─► extract text nodes ─► Messagevisor CSV ─► import --createMissing ─► translate (DeepL / AI / translator) ─► import --applyThe new piece is the leftmost step. Once you have a CSV in the standard Messagevisor shape, the rest of the round trip is the same as any other workflow in this section.
Prerequisites#
- A Figma file you have access to.
- A Figma personal access token.
- Node.js available on your machine.
- The file key from your Figma URL, e.g. for
https://www.figma.com/file/ABC123/My-Design, the file key isABC123.
Set the token in the environment:
$ export FIGMA_TOKEN=your-token-hereStep 1: Decide how text nodes map to message keys#
Messagevisor message keys come from file paths under messages/. To go from a Figma text node to a key, you need a naming convention the script can follow.
Two patterns work well:
Pattern A: name the Figma text node after the key#
Rename text nodes in Figma to match the intended message key:
auth.signinauth.signoutdashboard.welcomebilling.totalDesigners do this in the layers panel. The script then uses the node name verbatim as the message key.
This is the most explicit and most reviewable pattern. It does require designer cooperation.
Pattern B: derive the key from frame and node hierarchy#
If you cannot rename every text node, derive the key from the surrounding frame names:
Frame "Auth" ▸ Frame "Signin" ▸ Text "Sign in" -> auth.signinFrame "Dashboard" ▸ Text "Welcome back, {name}" -> dashboard.welcomeThis is fragile (renaming a frame changes the key) but works as a starting point.
The script below assumes Pattern A. Adapt the keyFromNode helper if you use Pattern B.
Step 2: Write an extraction script#
Create a script that walks the Figma file tree, finds text nodes whose names look like message keys, and writes them to a CSV:
import { writeFile } from "node:fs/promises";const [, , fileKey, sourceLocale, outputPath] = process.argv;if (!fileKey || !sourceLocale || !outputPath) { console.error( "Usage: node scripts/extract-from-figma.mjs <fileKey> <sourceLocale> <outputPath>", ); process.exit(1);}const response = await fetch( `https://api.figma.com/v1/files/${fileKey}?geometry=paths`, { headers: { "X-Figma-Token": process.env.FIGMA_TOKEN }, },);if (!response.ok) { console.error(`Figma API error: ${response.status} ${response.statusText}`); process.exit(1);}const file = await response.json();const rows = [];walk(file.document);function walk(node, ancestors = []) { if (node.type === "TEXT" && looksLikeKey(node.name)) { rows.push({ key: node.name, description: ancestors.map((a) => a.name).join(" ▸ "), value: node.characters ?? "", }); } if (Array.isArray(node.children)) { for (const child of node.children) { walk(child, [...ancestors, node]); } }}function looksLikeKey(name) { // Adjust this to your project's namespace character. return /^[a-z0-9]+(\.[a-z0-9-]+)+$/.test(name);}// Deduplicate: a text node may appear in multiple frames.const seen = new Map();for (const row of rows) { if (!seen.has(row.key)) seen.set(row.key, row);}const header = ["messageKey", "messageDescription", sourceLocale];const body = [...seen.values()].map((row) => [row.key, row.description, row.value]);await writeFile(outputPath, stringifyCsv([header, ...body]), "utf8");console.log(`Wrote ${body.length} rows to ${outputPath}`);function stringifyCsv(rows) { return ( rows .map((row) => row.map(csvCell).join(",")) .join("\n") + "\n" );}function csvCell(value) { const s = String(value ?? ""); if (/[",\n\r]/.test(s)) { return `"${s.replace(/"/g, '""')}"`; } return s;}Run it:
$ node scripts/extract-from-figma.mjs ABC123 en exports/from-figma-en.csvThe output CSV looks like:
messageKey,messageDescription,enauth.signin,Auth ▸ Signin,Sign inauth.signout,Auth ▸ Signout,Sign outdashboard.welcome,Dashboard,"Welcome back, {name}"For anything beyond a quick experiment, use a real CSV library like csv-stringify instead of the inline helpers above.
Step 3: Preview the Messagevisor import#
The CSV from Figma will usually introduce new message keys. Use --createMissing to allow that:
$ npx messagevisor import exports/from-figma-en.csv --createMissingRead the summary carefully. --createMissing is the flag most likely to introduce garbage if the extraction script has a bug or the Figma file has a node whose name accidentally matches the key pattern.
Common things to check:
- Are the number of
Created messagesclose to what you expected? - Are there any rows whose
messageKeylooks wrong (typos, casing, accidental dots)? - Are descriptions readable, or do they include layer names like
"Frame 47 ▸ Group 12"?
Fix issues at the source (rename nodes in Figma, adjust the script) and re-export before applying.
Step 4: Apply the import#
When the preview looks right:
$ npx messagevisor import exports/from-figma-en.csv --createMissing --applyMessagevisor creates the new messages under messages/, with translations under the source locale (en in the example above).
Step 5: Translate into other locales#
Once the source-locale messages are in the project, translate them like any other batch of new strings. Three reasonable next steps:
- Run the Google Sheets workflow to fill in a target locale via
GOOGLETRANSLATE. - Run the DeepL workflow to fill in a target locale via the DeepL API.
- Run the AI translations workflow to have an agent translate the strings and run the import for you.
- Send the new strings to a human translator once the source copy is locked.
All four workflows start with messagevisor export --onlyUntranslated, so they pick up exactly the keys you just imported from Figma.
Step 6: Validate and review#
The usual loop:
$ 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 design copy outside Figma, and it surfaces things like wrong-context overrides, missing descriptions, and copy that does not fit its UI slot.
Tips and variations#
Use a Figma plugin for the extraction#
The script above uses the REST API because it works without installing anything in Figma. If your team is happy installing a plugin, the Figma plugin API can extract text nodes from the open file with no token setup. The plugin can write a CSV to clipboard or save it locally.
A plugin works well if:
- Designers run the export themselves between iterations
- You want a "Send to Messagevisor" button inside Figma
- You do not want to manage a Figma token in CI
A REST script works well if:
- You want extraction to run in CI (for example, on a schedule that watches the design file)
- Engineers run the export, not designers
Keep designers honest with linting#
If you adopt Pattern A (text node named after the key), add a CI job that runs the extraction script and fails if any text node name violates the key pattern. It catches "the designer forgot to name this node" before it becomes a missing message.
Mark variant strings in Figma#
For copy that has an override (Pro plan vs free plan, web vs mobile), encode the override key in the node 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 extraction script writes these as override rows and the Messagevisor importer handles them the same way as a CSV from a translator.
Pair with pseudo-localization#
Once the source copy is in Messagevisor, generate a pseudo-localized target locale (longer strings, accented characters) and run the design through it before commissioning real translation. This catches layout problems early, before any translator time is spent.

