TL;DR
We built a multilingual localization pipeline on OpenClaw that takes English source posts, generates twelve locale variants, and routes each one through glossary control, automatic evaluations, and human approvals where needed. The core agent uses cached artifacts and idempotent writes so retries never duplicate work. A lightweight Slack step handles reviews and timeouts. The result is predictable throughput and consistent terminology. This story shows where AI agents fit and where we still need people in the loop. It took one week to reach production stability.
The brief: twelve languages without chaos
The marketing team wanted to publish every long form article in twelve locales within two hours of the English version going live. The initial approach was a chain of manual handoffs across three tools and constant reminders in chat. Terms drifted, screenshots were outdated, and no one could answer a basic audit question like which revision was live in French.
We started from a simple goal: a deterministic path from source to publish with clear stops for glossary checks, automatic quality gates, and fast human edits. OpenClaw was already the backbone for our workflows, and the agentic approach fit well with the rest of the stack. If you have not seen what ButterGrow does at a high level, the summary of AI marketing automation features is a useful primer.
Architecture overview
At a high level, each job is a content bundle with frontmatter, markdown, and media. The state machine fans out locales, runs translation with a model selected per language, applies a terminology pass, runs an evaluation, and then gates to either auto publish or a human approval path. All artifacts are content addressed so retries are safe and fast.
The pipeline is opinionated about data contracts. Every piece of text carries a required locale key that must parse as a valid BCP 47 tag. For formats, the agent prefers plain markdown with fenced code blocks and descriptive alt text for images, which keeps downstream renderers simple.
Step 1Model and glossary choices
We did not pick a single model for every language. Instead, the agent keeps a tiny registry of model preferences per locale and switches based on measured quality. Glossary control comes from a brand dictionary with two parts: a protected list of terms that must not be translated and a list of preferred equivalents for domain phrases. The dictionary lives as a versioned artifact that the agent mounts at runtime.
To avoid prompt bloat, the system prompt contains only guidelines, while the glossary is passed as a short JSON sidecar and interpolated into the translator prompt right before inference. Here is a simplified prompt builder:
type Glossary = {
protected: string[];
preferred: { [term: string]: string };
};
export function buildPrompt(glossary: Glossary, locale: string) {
const protectedTerms = glossary.protected.map(t => `- ${t}`).join("\n");
const preferredPairs = Object.entries(glossary.preferred)
.map(([k, v]) => `- ${k} => ${v}`)
.join("\n");
return `You are a professional marketing translator.
Target locale: ${locale}.
Keep these terms untouched:\n${protectedTerms}
When possible use these equivalents:\n${preferredPairs}
Preserve markdown and code fences exactly.`;
}
Step 2BCP 47 language tag validation
We learned early that small typos in locale codes ripple into big problems. A stray es_ES breaks formatting downstream, and pt is too vague for regional conventions. We added a preflight that normalizes tags like es-ES, pt-BR, and fr-CA and rejects anything that does not parse. The validator relies on a CLDR whitelist and a tiny linter node that returns a helpful error for editors.
For reference while designing this step, we leaned on the MDN documentation for the lang attribute to keep our mental model straight about language declarations, and we checked the Unicode CLDR locale data whenever we were unsure about pluralization rules or regional formats.
Step 3Human-in-the-loop approvals in Slack
The fastest edit is a single message with two buttons. Our approval step posts a short preview into a channel with the key paragraphs, a diff against the prior revision if one exists, and two actions: Approve or Request Edit. Approve continues the job to publish. Request Edit lets the reviewer add a short comment that returns the content to a revise step.
We borrowed patterns from Slack message design that we used in other projects and adapted them to this agent. If you want a deeper walkthrough of how to structure lightweight reviews, the write up on Slack content approvals with agentic workflows covers the interaction details from a separate build.
Step 4Caching and idempotency
Retries happen. Network issues, downstream rate limits, and small playbook changes are routine. The agent treats the translator as a pure function of (content_hash, locale, glossary_version, model_version). Outputs are stored at a path that encodes those inputs. On retry with identical inputs, the job bypasses inference and proceeds to evaluation. This saved a surprising amount of time and money in early testing.
The downstream publisher expects a receipt so audit trails are obvious. We write a tiny JSON record per locale with the content hash, the model id, the glossary version, and a final checksum. That receipt becomes the source of truth for compliance questions later.
Step 5Evaluation harness and backtests
We wanted a signal that gives us 80 percent confidence fast. The harness has three legs:
- A quality estimation prompt that scores fluency and adequacy on a 1 to 5 scale.
- A back-translation check that penalizes outputs that wander too far from the source.
- A lexical scan for glossary violations with a strong penalty for forbidden substitutions.
If the composite score drops below a set threshold, the job routes to a human reviewer. Scores are tracked per locale and per model so we can spot regressions after upgrades.
We also skimmed the MDN guide to the Accept-Language header for sanity on browser hints used later in targeting rules. It did not change the translation logic, but it clarified how we should log preferences for A/B tests.
The hairy problems we hit
No build like this ships clean on the first pass. Three issues stood out and cost real time.
Glossary collisions. The same English term could be protected in one context and mapped to a preferred equivalent in another. Our first pass tried to solve this in prompts. It turned out to be brittle. The fix was to add scopes to glossary entries and attach them to content types, so the translator knew when to treat a term as protected versus substitutable.
Formatting drift. Headings with emoji or non breaking spaces survived translation but broke the markdown renderer in some locales. We added a formatter pass that re serializes markdown and a small test set of edge cases. This caught a dozen tiny issues that would have been embarrassing in production.
Slow reviews in two languages. Legal wanted to look at German and French for specific phrasing. The first design blocked the entire batch until every locale was decided, which turned the pipeline into a traffic jam. We changed the state machine so each locale publishes independently, and the batch completes as a set of individual jobs.
What moved the metrics
After the first month we had enough data to compare versions. Two numbers mattered for the team: end to end lead time and post publish edits per locale.
- Lead time from English publish to the last locale going live dropped from an average of 14 hours to 95 minutes at P50. P90 settled at around 140 minutes. The best run finished in 62 minutes during a quiet window.
- Post publish edits fell by 38 percent due to glossary enforcement and the back translation gate. The biggest improvement showed up in Spanish and Portuguese, where a few recurring brand terms had caused most of the churn.
- Editors spent less time on tedious copy fixes. Average manual touches per batch dropped from 24 to 9, and reviewers focused mostly on idioms and tone.
These are small wins compared to a full localization team, but they are real and repeatable for our content scale.
What we would change next time
We would start with scoped glossaries on day one. The collision issue was predictable, and we paid the price for trying to keep the first version simple.
We would also invest earlier in a small test corpus with tricky examples. The time to build those ten samples is tiny compared to the time we spent chasing formatting issues later.
Finally, we would give legal a dedicated lane in the state machine from the start. The independent publish model worked well, but this path should have been obvious earlier.
Implementation snippets
Here is a condensed version of the state machine structure that carried most of the load. Real code is larger, but this shows the main ideas.
name: multilingual_localizer_v3
version: 3
inputs:
- source_markdown
- glossary_version
- locales
states:
- id: validate_locales
type: task
run: locale-validator
on_error: fail
- id: fanout
type: map
for_each: ${locales}
iterator: locale
states:
- id: translate
type: task
run: translator
with:
locale: ${locale}
glossary_version: ${glossary_version}
cache_key: ${hash(source_markdown)}-${locale}-${glossary_version}-${model_id}
- id: evaluate
type: task
run: evaluator
with:
locale: ${locale}
- id: gate
type: choice
when:
- condition: ${evaluate.score >= 0.82}
goto: publish
- condition: true
goto: slack_review
- id: slack_review
type: task
run: slack-approval
timeout_minutes: 20
on_approve: publish
on_request_changes: revise
- id: revise
type: task
run: post-editor
goto: evaluate
- id: publish
type: task
run: publisher
finalize:
- write_receipt
- id: done
type: end
The translator and evaluator are small workers that read from an object store and write a new artifact whose key encodes the content hash and locale. The publisher writes receipts for traceability. The Slack step links back to the job details so editors can see context quickly.
If you want to see how playbooks and templates evolve for this kind of build, the overview on reusable playbooks and versioning pairs well with this story. For a broader survey of product capabilities relevant to this work, the page that summarizes what ButterGrow does offers a grounded tour.
The fastest way to try this pattern is to clone a minimal playbook and swap in your glossary and locales. The onboarding guide will walk you through keys and workflow setup so you can run a first batch end to end.
ButterGrow runs the hosted OpenClaw assistant so you do not have to wire these pieces together yourself. If you want to try the localizer pattern or a simpler translation path, you can get started in minutes and adapt the reference playbook to your stack. If you are still exploring broader options, you can find more from the ButterGrow blog and browse other builds for inspiration.
References
- MDN documentation for the lang attribute - Background on language declarations and examples.
- Unicode CLDR locale data - Authoritative data for locales, plural rules, and formatting.
- MDN guide to the Accept-Language header - How clients express language preferences.
Frequently Asked Questions
How did you enforce BCP 47 language tags and locale validation in the workflow?+
We normalized every inbound locale to a canonical BCP 47 tag and rejected anything that did not parse. A small validator node checked tags like "es-ES" or "pt-BR" against a CLDR whitelist before the job reached the translation step.
What is the best way to inject a brand glossary into a translation agent on OpenClaw?+
We mounted the glossary as a versioned artifact and merged it into the system prompt at runtime. The agent also loaded term replacements from a sidecar JSON file so updates did not require redeploying the state machine.
How did human-in-the-loop reviews work without slowing the pipeline?+
We used a Slack approval step with a 20 minute timeout and an auto-approve fallback for low risk languages. Editors could request a single-shot revise that reentered the state machine at the post-edit step without restarting the entire job.
What caching and idempotency patterns kept retries from duplicating work?+
We keyed artifacts by a deterministic content hash plus locale so retries were read-only when identical inputs recurred. The agent wrote a small receipt to an idempotency table so upstream publishers could prove what was shipped and when.
How did you evaluate translation quality without human ratings for every string?+
We combined a QE prompt with back-translation and a lexical penalty for glossary violations. Samples that crossed a risk threshold were escalated to human reviewers and the scores were tracked per locale to tune model selection.
Which OpenClaw features mattered most for this build?+
Playbooks and state machines gave us deterministic paths for approvals and retries, while observability and draft runs made triage safer. The features overview in ButterGrow maps closely to what we used in production.
Ready to try ButterGrow?
See how ButterGrow can supercharge your growth with a quick demo.
Book a Demo