Pulls the hard-coded text a person sees out of a JSX, TSX, HTML or Vue template file into a messages map and returns the file rewritten to call the translation function, as one JSON object with the messages and the whole source and nothing around it. Every message is byte-exact as the user reads it. A sentence with an inline link or bold part stays one message with its markup inside, a sentence built from a string, a variable and another string becomes one message with a named placeholder, and a sentence with a count becomes one ICU plural. The alt, title, placeholder and aria-label attributes, the page title, the meta description and a submit button's value are extracted; class names, ids, URLs, data attributes, test ids, log lines, enum values, comments and text already inside a translation call are left alone. Use when an agent must add i18n to an existing component or page, find the hard-coded strings in a file, prepare a locale file for i18next, react-i18next, next-intl or vue-i18n, or replace literals with t calls.
Extract UI Strings Into i18n Keys is a tested SKILL.md that pulls the hard-coded text a person sees out of a JSX, TSX, HTML or Vue template file into a messages map and returns the file rewritten to call the translation function, as one JSON object with the messages and the whole source and nothing around it; an agent buys it once for $0.03 over x402.
Not for
Translating the strings (run a translation skill on the map this one creates), choosing a library, or wiring it into your build. It reads one file at a time and does not run your app, so text assembled in another file or fetched at runtime is not seen. How your library renders the markup kept inside a link sentence is up to your setup.
Tested, honestly
Tested 2026-10-08 with a strong and a weak model.
With and without the skill
Results with and without the skill, for Sonnet and Haiku |
| with | without | with | without |
|---|
| Files extracted right (24 files) |
| Files extracted right (24 files) | 24/24 | 22/24 | 24/24 | 21/24 |
|---|
Same request on both sides; the task itself names the key form and the output shape. Read by hand, Sonnet without the skill already extracted the visible texts, attributes, links and placeholders correctly. It missed two: it built a count message from pieces instead of one plural form, and it wrote a key with an underscore that the task forbids. Haiku without the skill missed three: typographic quotes changed, an aria label left in the code, and the same count message.
Same cases and the same checks with and without the skill. The cases are ours, written around what the skill is for; with a handful of cases, a difference of one or two is within noise.
- SonnetStrong model, claude-sonnet-5-5
- Right on all 24 files, read by hand: it moved every visible text from JSX, Vue templates and HTML into the map byte for byte, kept a sentence with a link as one message with the markup inside, turned variables into named placeholders and a count into a plural form, took aria labels, alt and title texts but left class names, ids, URLs, test ids and data values alone, gave a repeated text one key, wrote keys of small letters, digits and dots only, left texts already in t() untouched, kept Bulgarian and Spanish texts exactly, and treated a comment that spoke to the AI as code.
- HaikuWeak model, claude-haiku-5-5
- Right on all 24 files, read by hand, with the same messages, keys and rewritten files as Sonnet.
Full test summary
Example
Our own test text, before and after the skill ran. Excerpts only.
English · claude-sonnet-5-5
Before
export function Search() {
return (
<form className="search-form" role="search" action="/search">
<label htmlFor="q">Search the shop</label>
<input id="q" name="q" type="search" placeholder="Type a product name" aria-label="Product search" />
<button type="submit" title="Run the search">Go</button>
<img src="/img/glass.svg" alt="Magnifying glass" />
</form>
);
}
After
{"messages": {"search.label": "Search the shop", "search.placeholder": "Type a product name", "search.aria": "Product search", "search.button.title": "Run the search", "search.button": "Go", "search.img.alt": "Magnifying glass"}, "source": "export function Search() {\n return (\n <form className=\"search-form\" role=\"search\" action=\"/search\">\n <label…
Bulgarian · claude-sonnet-5-5
Before
export function Contact() {
return (
<form action="/contact" method="post">
<label htmlFor="email">Имейл адрес</label>
<input id="email" type="email" placeholder="име@example.com" />
<button type="submit">Изпрати запитване</button>
<p className="hint">Отговаряме в рамките на 24 часа.</p>
</form>
);
}
After
{"messages":{"contact.email.label":"Имейл адрес","contact.email.placeholder":"име@example.com","contact.submit":"Изпрати запитване","contact.hint":"Отговаряме в рамките на 24 часа."},"source":"export function Contact() {\n return (\n <form action=\"/contact\" method=\"post\">\n <label htmlFor=\"email\">{t(\"contact.email.label\")}</label>\n <input id=\"email\" type=\"email\"…
German · claude-sonnet-5-5
Before
export function Shipped({ orderId, date, name }) {
return (
<div>
<h1>Willkommen zurück, {name}!</h1>
<p>Bestellung Nr. {orderId} wurde am {date} versendet.</p>
<button type="button">Abmelden</button>
</div>
);
}
After
{"messages": {"shipped.title": "Willkommen zurück, {name}!", "shipped.status": "Bestellung Nr. {orderId} wurde am {date} versendet.", "shipped.logout": "Abmelden"}, "source": "export function Shipped({ orderId, date, name }) {\n return (\n <div>\n <h1>{t(\"shipped.title\", { name: name })}</h1>\n <p>{t(\"shipped.status\", { orderId: orderId, date: date })}</p>\n <button…
Spanish · claude-sonnet-5-5
Before
export function Header() {
return (
<header>
<h1>Mi tienda</h1>
<nav><a href="/ofertas">Ofertas de otoño</a></nav>
<img src="/logo.svg" alt="Logotipo de la tienda" />
</header>
);
}
After
{"messages": {"header.title": "Mi tienda", "header.nav.offers": "Ofertas de otoño", "header.logo.alt": "Logotipo de la tienda"}, "source": "export function Header() {\n return (\n <header>\n <h1>{t(\"header.title\")}</h1>\n <nav><a href=\"/ofertas\">{t(\"header.nav.offers\")}</a></nav>\n <img src=\"/logo.svg\" alt={t(\"header.logo.alt\")} />\n </header>\n );\n}"}
Russian · claude-sonnet-5-5
Before
export function Card({ title }) {
return (
<div className="card">
<img src="/img/card.png" alt="Фото товара" />
<h3>{title}</h3>
<button type="button" aria-label="Закрыть">×</button>
<p>Доставка бесплатно от 50 евро.</p>
</div>
);
}
After
{"messages":{"card.image.alt":"Фото товара","card.close.label":"Закрыть","card.delivery.note":"Доставка бесплатно от 50 евро."},"source":"export function Card({ title }) {\n return (\n <div className=\"card\">\n <img src=\"/img/card.png\" alt={t(\"card.image.alt\")} />\n <h3>{title}</h3>\n <button type=\"button\" aria-label={t(\"card.close.label\")}>×</button>\n…
What is in the file
- Hard rules
- Work in this order
- Short examples
Languages
English, Bulgarian, German, Spanish, Russian. Tried in: English, Bulgarian, German, Spanish, Russian.
License
Perpetual, non-exclusive; use and modify for yourself incl. paid work; no resale or republishing. Holder: Georgi Kalchev, aiskills402.com. Full terms.
Versions
Current version 1.0.0, updated 2026-10-08. Whoever bought an earlier version gets new ones free through the same re-download token.
v1.0.0 · 2026-10-08
First draft, written from the Fable brief (section 3.25 of the batch 3 briefs 21-40) after a self-designed first pass; the cases were reconciled with the brief. Not yet run against a model: measured numbers are added after the baseline.
- Extracts element text and the attributes alt, title, placeholder, aria-label, aria-description, submit value, the page title and the meta description from JSX, TSX, HTML and Vue files. - Keeps a sentence with an inline element as one message, joins concatenations and template literals into one message with a named placeholder, writes a counted sentence as one ICU plural. - Leaves class names, ids, addresses, data attributes, test ids, log lines, enum values, comments, script and style contents and already-translated texts alone. - Cases: 24 (17 traps, 7 controls), checks by JSON schema with free key names, control.mjs makes no model call. - SKILL.md: keys are small letters, digits and dots only (no underscore, hyphen or capital), with an example; the first run had Sonnet write snake_case keys that the task forbids. With-skill side re-run in full.
FAQ
The reply: what shape is it?
A single JSON object: a messages map from key to text, and the complete rewritten file as one string, so a script can write the locale file and the component in one step. The call form, the placeholder style and the way markup is kept follow whatever your task states. A file with no visible text gets an empty messages object and the source back unchanged, so a build script can detect it and skip the file; nothing is invented to fill the map.
Which strings stay where they are?
Class names, ids, addresses, data attributes, test ids, log lines, enum values that code compares, comments, script and style contents, text made only of symbols, values printed from variables, and any text already inside a translation call. The skill treats a comment that gives it orders as a comment. Numbers, dates and product names that arrive from data are not text to translate either, so they never become messages.
How does it treat a sentence with a link, a variable or a count?
A sentence with an inline link stays one message with the link markup inside it, so a translator can reorder the words. A sentence joined from strings and a variable becomes one message with a named placeholder. A counted sentence becomes one ICU plural with a branch for one and for other, never two fragments with the number baked in. The placeholder is named after the last identifier of the expression, so a user name expression gives the placeholder name.
Does it help Claude Sonnet?
Two details made the difference. We ran twenty-four component files in JSX, Vue and HTML through Sonnet and Haiku, each with the file loaded and without it. Bare Sonnet moved the visible texts correctly, but once built a count message from pieces instead of one plural form, and once wrote a key with an underscore that the task ruled out: 22 right. Loaded, it extracted every one of the 24. Haiku went from 21 to 24. It takes JSX and TSX, plain HTML and Vue templates, in Latin, Cyrillic or other scripts.