How Batch Translation Workflows Break Continuous Deployment
Continuous deployment promises one thing above all: any change, from a one-line bug fix to a new feature, can move from commit to production safely and on its own schedule. Most teams have spent years getting there. Then they add a second language, and a step quietly reappears in the middle of the pipeline that nobody automated: the batch translation handoff.
It does not look like a deployment problem at first. It looks like a process: export the strings, send them to a translation vendor, wait, get files back, import them, resolve the conflicts, redeploy. But that round-trip is a release-blocking step wearing the costume of a workflow, and it breaks continuous deployment in specific, predictable ways.
This is the engineer's version of why, and what the continuous alternative actually looks like when it is wired into CI/CD.
- Batch translation: export to files, hand off to a vendor, wait, reimport, redeploy. A serialized, out-of-band step inside an otherwise automated pipeline.
- The break: translations land after the feature shipped, reimports conflict with code that moved on, and a one-word fix needs a full redeploy. Teams compensate with a content freeze.
- Continuous localization: keys reach the TMS from your code, a CLI keeps things in sync in CI, and translations ship over a CDN without a redeploy.
- Quality, continuously: AI auto-translation plus Quality Estimation handles the bulk; only low-confidence strings are routed to human review.
How a batch translation workflow breaks CI/CD
Continuous deployment works because every step is automated, fast, and reversible. A batch handoff is none of those. Here is where it actually breaks.
It serializes a parallel process. Development is continuous; a batch handoff is a stop. You finish a feature, then you wait days or weeks for translations before the localized version can ship. The feature is done, the translation is not, and the release is held hostage to the slowest language. Continuous deployment was supposed to remove exactly this kind of gate.
The reimport conflicts with code that moved on. By the time translated files come back, the codebase has changed: keys were renamed, strings were split, a screen was refactored. Merging the returned files is a manual conflict-resolution exercise, the same problem a long-lived branch creates, except the branch is a folder of .json or .xliff files maintained by people outside your repo. The longer the batch, the worse the merge.
A one-word fix becomes a full release. Found a typo in the German copy, or a string that overflows its button? In a batch-and-bundle model the corrected file has to be committed, built, and redeployed through the entire pipeline. Text that has nothing to do with code is now gated behind a code deploy. That is backwards.
It pushes teams into a content freeze. Because the round-trip is slow and risky, the common defense is to lock all strings two to three months before a launch so there is time to translate. Now the fast-moving half of the product (the copy) is frozen to accommodate the slow half (the batch). The hidden costs of this, the coordination, the re-roundtrips, the delayed rollout, usually dwarf the cost of the translation itself.
It has no shared state. Files in flight to a vendor have no record of who reviewed what, which strings are approved, what the agreed terminology is, or how confident any given translation is. That state lives in email threads and spreadsheets, not in your pipeline. CI cannot reason about it, so CI cannot gate on it.
None of this is the translators' fault, and it is not solved by translating faster. It is an architecture problem: a serialized, stateful, manual step was dropped into a pipeline built to be parallel, stateless between runs, and automated.
What continuous looks like, wired into the pipeline
The fix is to treat translations the way you already treat code and config: they flow through the pipeline continuously, they have their own delivery channel, and they never block a release. Concretely, with i18next and Locize, there are three moving parts.
1. Keys reach the TMS from your code, not a spreadsheet
When you write a new feature, the new translation keys should appear in the translation system the moment they exist in code. i18next's saveMissing does this: any key that has no value yet is reported automatically.
import i18next from 'i18next'
import LocizeBackend from 'i18next-locize-backend'
i18next.use(LocizeBackend).init({
// ...
saveMissing: true, // new keys show up in Locize as you code
backend: {
projectId: '<your-project-id>',
apiKey: '<your-api-key>' // development only, never ship this to production
}
})This runs in development, from the reference language only, so the source of truth for "what needs translating" is your actual code, not a manually maintained export. There is no batch to assemble because the list is always current.
2. A CLI step keeps things in sync inside CI
In your pipeline, the locize CLI reconciles your local reference strings with the project and pulls translations back. It does a real diff (add, update, remove) rather than a blind overwrite, and you can make it conservative for automated runs:
# In CI: push reference keys, pull translations, do not delete anything automatically
locize sync \
--project-id <your-project-id> \
--api-key $LOCIZE_API_KEY \
--ver latest \
--path ./locales \
--update-values false \
--skip-delete trueBy default sync only inspects the reference language (--reference-language-only true) and backs up any segments it would remove before touching them. Run it with --dry true first to see the diff without writing. The point is that translation drift becomes a visible, scriptable step in CI instead of a manual merge after the fact.
3. Publish a pinned version at deploy time
This is the part that actually makes a release atomic. Your app loads translations from a named version (think latest for the working copy, production for what ships), and you publish that version at exactly the moment you deploy:
# Final pipeline step, alongside your code deploy
locize publish-version --ver production --project-id <your-project-id> --api-key $LOCIZE_API_KEYBecause the running app is pinned to a version rather than reading the live working copy, in-progress edits never leak into production mid-deploy. And because translations are delivered over a CDN, a copy fix published between releases reaches users without a redeploy. The i18next backend refetches on its own interval; you ship the German typo fix in seconds, not in a release.
The release stays atomic and reversible. Copy changes between releases ride a separate, faster channel. Neither one blocks the other.
For a deeper background on how this model evolved, see modern continuous localization and the original take on continuous development, integration and localization.
Keeping quality without a batch review gate
The honest objection to "ship translations continuously" is quality. The whole reason batches exist is the human review step. But you do not need a batch to get review; you need the review to be continuous and selective instead of all-at-once.
When a new key arrives, Locize auto-translates it. You can bring your own provider key (OpenAI, Gemini, Mistral, DeepL or Lara) or use the built-in AI, and the result is then scored by Quality Estimation on a 0 to 1 scale. The routing is the important part:
- Translations above the threshold are saved and can auto-publish.
- Translations below the threshold, or flagged with a major issue, are routed into the human review workflow.
So instead of one person checking thousands of strings in an exported file once a quarter, your reviewers see only the strings that actually need a human, in context, as they appear. A glossary and styleguide keep terminology and brand voice consistent across all of it. The review gate still exists; it just stopped being a batch.
Roles (admin, manager, publisher, translator), per-language and per-namespace scopes, and an in-context editor mean translators and reviewers work directly against the live project. There is no export to send out and no file to merge back.
How to get off batch
You do not have to rebuild everything at once. A pragmatic migration:
- Pin your app to a version. Point production at a named version (for example
production) instead of the live working copy. This alone makes deploys atomic. - Move the freeze to a publish. Replace the content freeze with publishing that version at deploy time. Strings stay editable up to the moment you ship.
- Put
syncin CI. Add the CLI sync step so reference-language drift is caught automatically, starting with--skip-delete trueuntil you trust it. - Turn on auto-translation plus QE. Let AI cover the volume and route only low-confidence strings to humans, so the review queue is small and continuous.
- Branch the risky changes. For a big rename or restructuring, use a branch project so the experiment never touches the production version until it is merged.
Each step removes one piece of the batch handoff. By the end, translation is just another thing your pipeline does continuously, and "wait for translations" is no longer a line item in your release plan.
Where Locize fits
Continuous deployment broke the batch model for code a decade ago. The same logic applies to the strings: a serialized, manual, stateful handoff has no place in an automated pipeline. Locize is built around that idea, keys from your code, a CLI for CI, version-pinned publishing, CDN delivery, and AI translation gated by Quality Estimation, so localization stops being the step that blocks the release.
If your translation process still has a "send the files out and wait" phase, that is the part of your pipeline continuous deployment never reached. See how continuous localization works, or create a free Locize account and wire it into your next deploy.
Tired of managing translations by hand?
Locize is the translation management backend by the i18next team: CDN delivery, AI translation, in-context editing, no redeploys.
Start your free 14-day trial