The integration between three teams was a designer
Design produced the strings, localisation owned the store they lived in, engineering consumed the keys, and nothing spanned the three, so a designer did the spanning by hand: up to four hours on one flow. I built a Figma plugin that maps each text layer to a key in the store, generates new keys checked against the ones already there, translates in the brand voice and builds the localised frames. The design team uses it, and it saves an estimated two hours of design work per release.
- Product
- Internal tooling for the design team of a nutrition tracking app, ~1.3M monthly active users
- Platforms
- Figma plugin, TypeScript and React
- My role
- I found the problem, decided what the tool should do and how it should behave, and vibecoded it. The product decisions are mine. The code is not: I did not write it and I am not presenting this as engineering work.
- Team
- I built it on my own. The design team is two to three designers including me, and all of them use it.
- Duration
- Built September to October 2026
- The constraint
- No budget for a backend, no licence to put an API key in a bundle every designer installs, and a per-call cost on every translation
- Outcome
- An estimated two hours of design work saved per release, in use across the design team

Outcome
Work the designer used to do by hand became a tool the whole team runs.
up to 4h
of a designer’s time on one flow, before this existed
Naming each key and establishing it was unique, then building the handoff table. It scaled with how many new strings the flow introduced.
~2h
design work saved per release
My own estimate, not a measurement. There is a caveat on this number below.
2 to 3
designers using it, which is the whole team
A small team, so this is full adoption rather than wide adoption. Both are true and the second would be overstating it.
Where this started
Nobody asked for this one either.
There was no brief. Three things arrived at once: I was doing the manual version again, I found duplicate keys in the string store for strings that already had one, and a mockup had drifted from the copy that actually shipped.
I built it myself rather than pitching it, and asked for the API tokens it needed as I went. The head of product backed it later by supporting the copy service side, which is what turned a thing I had made into a thing the team could rely on.
How it worked before
The wireframes were never localised. What the designer owed was the step before translation.
Nobody expected localised mockups and nobody made them. What was expected of the designer was the handoff: inventing a localisation key for every new string in the flow, then assembling a table of keys and strings for machine translation. Key creation, table creation, copy-paste. On a flow that introduced a lot of new strings, up to four hours.
The part that actually cost the time was not typing. Creating a new key means establishing that it is new, and a designer working by hand has no practical way to see the keys that already exist. Either you spend the time checking or you skip it and make a duplicate.
So the thing worth automating was never translation. It was a designer standing in as the connection between the design file and the string store, by hand, on every flow.
The reframe
The problem sat between teams, which is why nobody had fixed it.
Read as
A slow piece of design work. The manual part of the handoff takes too long, so it should take less.
Reframed as
Work that belongs to no single team. Design produces the strings, localisation owns the store they live in, engineering consumes the keys, and nothing spanned the three. A designer was doing the spanning by hand, which is why design speed was always set by the localisation step.
Naming a key is the clearest symptom. To create one you first have to know whether it already exists, which means knowing what another team’s store already holds, and a designer had no way to see that. So you either pay in time or you pay in duplicates, and a duplicate gets the same sentence translated twice across 13 languages, charged twice, and left free to drift until the mockup and the shipped copy disagree.
Exploration
What I built first and then took out.
The plugin holding the brand voice itself
The first version built its own tone-of-voice prompt and carried the voice guide in its source, so the plugin decided how the brand sounded in each language.
I took it out. Translation and key generation now happen entirely in the copy service, which applies the voice server-side. That unified the translation service across everything that uses it, and it left the plugin with no dependency on an AI API key of its own.
Two modes, because there turned out to be two audiences
The version that shipped reads the selected frame and suggests its own path: Manage keys when layers already carry keys, Translate when none do. Manage is the three-step sequence of assigning keys, filling translations and generating frames. Translate skips the key machinery entirely and goes straight to translating what is on the frame.
Translate was not a fallback or a first draft left in place. It is what the marketing designers use, and they use it because none of the copy they work on belongs in the product string store. Keeping a mode that answers a different job is the reason the tone channels cover email, social and LinkedIn as well as product.
The design
The plugin reads a frame the way the string store reads it: every text layer is a key, a deliberate skip, or not text at all.
Scanning a frame sorts its text layers into three kinds. Layers already mapped to a key show that key and where it came from. Layers the designer marks as dynamic are skipped on purpose and stay skipped. And layers whose content is generated at runtime, a calorie count or a percentage, are recognised as such and never asked about.
Assigning a key renames the layer to carry it. That one move does two jobs at once. Another designer opening the file reads straight off the layers panel which strings already have keys and which do not, without running anything. And the next time the plugin scans that frame it reads the same names back and knows the mapping is already done, so nobody is asked to redo work the file is already holding.
From there the work is a sequence rather than a screen: assign the keys, fill the translations for the languages and tone channel you picked, then generate one frame per language beside the original. The handoff out is a CSV shaped for import into the string store.
That is the path for product work. There is a second one that skips keys altogether and translates the frame as it stands, which is what the marketing designers use, since the copy they work on never goes into the product string store. The plugin reads the frame and proposes whichever path fits what it finds.
Three steps, each one counting what is still missing.

Assign keys

Pick languages

Generate
Why frames and not just a table
A spreadsheet cannot show you that German does not fit.
The export was always going to be a table, and a table is enough to get strings translated. It is not enough to tell you what the translation does to the layout. Generating a frame per language puts the result back in the place where length is a design problem rather than a cell.
The first run proved the point. In the German frame the opening letter of Aufgenommen is clipped by the metric that holds it, while the same string sits perfectly happily in the exported CSV. Nobody reading the table would have caught it.
Key decisions
The decisions that make it usable a month later, not just once.
The key lives in the layer name
Assigning a key renames the layer to carry it as a tag, and skipping a layer tags it the same way. The two tags are mutually exclusive: assigning a key clears the skip, skipping clears the key.
This is the decision the rest depends on, and the reason it is the layer name rather than hidden plugin data. The mapping survives duplicating the frame, copying it into another file, and handing the file to a machine that has never run the plugin. State kept anywhere else would not have.
Runtime text is skipped only where it is unmistakable
Numbers, units, placeholders and times are picked out at scan and left alone. The test is deliberately strict: a layer is only skipped when it is plainly a dynamic value that no language would translate anyway. Anything it is less sure about still gets asked, and the designer skips it by hand.
In a nutrition tracker most numbers on screen come from the backend, so localising the static layer achieves nothing. Which way to err was the actual decision. A dynamic layer the plugin asks about costs one click. A real string the plugin decides to skip on its own is one that never gets localised and that nobody notices is missing, so it only stays quiet when it is certain.
Each key says where it came from
A key already in the file shows plain, a key matched from the string store shows one badge, a key the service just generated shows another.
Without this the export cannot be trusted: nothing would distinguish what is already canonical from what a model invented a second ago.
Assigning an existing key rewrites the design, not the store
Mapping a layer to a key that already exists replaces the text in Figma with the source copy held against that key.
Something has to win when the two disagree, and I made it the store. That is the difference between mapping a design to the copy and translating the copy inside it.
Warnings state the cost and let you continue
Layers without a key and empty translation cells are counted, shown on the step pills, and spelled out as what will happen: those layers will not be translated, those cells will be empty in the generated frames.
The tool never blocks the designer. It names the price of continuing and lets them decide, because a half-localised frame is often exactly what someone wants at that moment.
A failed batch costs cells, not a language
When a batch fails, its cells are left empty and the rest of the language carries on. Re-running fills only the empty cells.
Across this many languages and batches some requests were always going to fail, so re-running had to be safe and cheap rather than destructive. That made idempotency a design requirement, not an implementation detail.
Generated frames stack to the right of the last one
Each new language frame is placed past the rightmost frame already generated from that source.
Thirteen languages generated into the same spot would land as thirteen overlapping frames.
The export asks which keys it is for, and writes what the importer expects
Exporting offers new keys only or every mapped key, with the count beside each. The file itself is written to the shape the string store’s own importer takes: a key column, the English source, then one column per language, quoted to the CSV standard and saved with a byte order mark so accented characters survive the trip.
For importing the new ones are the usual answer, since the existing keys are already there, and getting that wrong means re-uploading hundreds of rows that are already correct. Matching the importer’s format matters just as much: the file goes straight in, rather than becoming a reshaping job for whoever receives it.
Showing its work
Nothing a model produced is allowed to look like something a person approved.

Suggested, not applied

New versus existing

Reused versus generated
Generated keys queue up as suggestions rather than being applied. Once approved they carry a NEW badge, so an existing key and a minute-old one never look alike. Translations that already existed in the store are badged too, which is why the brand term Keto Burn comes back as Keto Burn in all three languages instead of being helpfully translated.

Selecting layers on the canvas filters the list to them, with a way back to the full set. The panel and the canvas stay one place to look instead of two lists to reconcile by eye.

The export asks which keys it is for, and the counts answer it: 36 of the 37 keys in this frame are new to the store.
Validation
Handing it to the team was the only real test, and it found less than I expected.
I expected handing it over to surface real problems, and it mostly did not
Nothing substantial broke once the rest of the team started using it. What came up was small, and the one real gap is that the plugin does not see SF Symbols, so text built from them is invisible to a scan.
That gap is still open. It also means this tool is validated mainly by my own use rather than by anyone else’s, which is the thinnest part of the case and worth saying out loud rather than leaving for someone to notice.
Impact
The localisation handoff stopped being something a designer assembles by hand.
~2h
design work saved per release
Estimated, not measured
2 to 3
designers using it, which is the whole team
As of October 2026
13
languages reachable from a frame in one pass
Across 6 tone channels, and localised frames were not something the team had at all before
On the numbers
This is internal tooling, so there is no product analytics behind it.
The before figure is solid because the work was mine: up to four hours on a flow with a lot of new strings. The two hours saved per release is an estimate drawn from that rather than a measured before and after, and the two sit in different units. How many flows went into a release varied too much to put a number on, so I am leaving both figures as they are instead of multiplying them into something tidier than the evidence supports.
One thing I am deliberately not counting is the localised frames. Nobody made those before this existed, so they are a capability the team did not have rather than time it got back. The saving is in naming keys and building the table.
Looking back
Putting the brand voice inside the plugin was a mistake.
The first version translated inside the plugin and carried the tone-of-voice guide in its own source, so the plugin decided how the brand sounded. I changed it, and the change was right: translation now runs in the copy service, which unifies it with everything else that translates, and the plugin no longer depends on an AI API key at all.
My role
Who did what
- Me
- The idea, the product decisions, the design, and the behaviour of every part of it. The plugin is vibecoded: I specified how each piece should work and a model wrote it. Deciding that the proxy was needed at all and how it should be gated were my calls, and I deployed it. The two tools the plugin calls on the copy service are mine as well.
- Product
- No product manager on it. I decided it was worth doing and built it. The head of product backed it afterwards by supporting the copy service side.
- Engineering
- Nobody built this with me, and the key format was never mine to agree: it is dictated by the tool the keys are stored in.
- Copy and localisation
- The copy service that holds the brand voice and glossary already existed. I extended it with the two tools the plugin needs, one to translate a batch of strings and one to generate a key that is checked against the keys already in the store.
- What outlived the project
- All of it is still running. The plugin is in use, the two tools I added to the copy service are available to anything else that needs them, the tag convention in layer names is read and written by people directly, and the proxy has kept serving without being touched.