Hey everyone, Alex here. Welcome back to "Coding with Alex" on sysseder.com.
If you've been browsing the Hacker News job boards recently, you might have spotted a fascinating hiring trend. Among the usual sea of "Senior Full-Stack Engineer" and "DevOps Specialist" postings, YC-backed startup Lingo.dev put out a call for a "Senior Content Engineer".
At first glance, you might think, "Oh, that's just a fancy word for a technical writer or a developer who likes writing documentation." But you'd be wrong. "Content Engineering" represents a massive structural shift in how modern, internationalized applications manage their copy, translation pipelines, and UI strings. It is where ASTs (Abstract Syntax Trees), CI/CD pipelines, LLMs, and internationalization (i18n) frameworks collide.
Today, we are going to dive deep into the technical challenges of modern application localization, why the traditional ways of managing copy are fundamentally broken, and how we can build robust, developer-friendly localization pipelines using modern tools.
The Broken Legacy of gettext and Static JSON Files
For decades, internationalization followed a predictable, painful pattern. If you were building a web app, you would use a tool like GNU gettext or maintain massive, deeply nested static JSON files (like en.json, es.json, and ja.json).
It usually looked something like this in your React or Next.js codebase:
// The standard, brittle way of doing localization
import { useTranslation } from 'react-i18n';
export function WelcomeMessage({ user }) {
const { t } = useTranslation();
return (
<div className="welcome-banner">
<h1>{t('dashboard.welcome', { name: user.name })}</h1>
<p>{t('dashboard.subscription_status', { count: user.daysLeft })}</p>
</div>
);
}
And then, buried in your public/locales/en.json file, you'd have:
{
"dashboard": {
"welcome": "Welcome back, {{name}}!",
"subscription_status": "You have {{count}} day left.",
"subscription_status_plural": "You have {{count}} days left."
}
}
This approach is riddled with technical and operational debt:
- Zero Type Safety: The translation keys (e.g.,
'dashboard.welcome') are magic strings. If a developer typos a key, the application silently fails or renders a ugly fallback string in production. - Grammar and Pluralization Nightmares: Not all languages handle plurals like English. Some languages (like Polish or Arabic) have multiple plural forms depending on the count. Hardcoding "one" vs. "other" logic in JSON files is incredibly fragile.
- The "Translator-Developer" Bottleneck: Product managers or professional translators cannot edit code. When copy changes, they must send a spreadsheet to a developer, who manually updates the JSON files, creates a pull request, waits for CI/CD, and deploys. This is a massive waste of engineering resources.
Enter Content Engineering: Bridging the Gap with ASTs
This is where Content Engineering comes in. Instead of treating localization as an afterthought managed via static assets, content engineering treats application copy as structured data that is deeply integrated with the codebase through compilation and static analysis.
Let's look at how modern localization engines use Abstract Syntax Trees (ASTs) to automate this process. Instead of manually writing JSON files, we can write our UI code using inline, type-safe components. A compiler then extracts these strings automatically during the build process.
How Inline Extraction Works (The Tech Behind It)
Imagine writing your code like this, using a modern library like LinguiJS or Paraglide JS:
import { Trans } from '@lingui/macro';
export function ActiveUsers({ count }) {
return (
<p>
<Trans>
There are <strong>{count}</strong> active users online.
</Trans>
</p>
);
}
Notice how we didn't specify a translation key. We didn't open a JSON file. We just wrote natural English inside a <Trans> macro.
During the build step, a Babel plugin or a Vite transformer parses this React code into an AST. It searches for the <Trans> JSX elements, extracts the default text, hashes it to generate a unique ID, and compiles it into a lightweight, optimized runtime bundle.
Here is a simplified mental model of how a Babel/SWC parser extracts this string:
// 1. AST Parser detects the JSXOpeningElement named "Trans"
// 2. It extracts the children: ["There are ", {count}, " active users online."]
// 3. It generates a stable message ID using a hashing algorithm (e.g., MurmurHash)
// Message ID: "a1b2c3d4"
// 4. It writes this to a translation template file (POT or JSON) automatically:
{
"a1b2c3d4": {
"message": "There are {count} active users online.",
"origin": [["src/components/ActiveUsers.jsx", 5]]
}
}
This eliminates the "magic string" problem entirely. The source of truth is your actual UI code, and the translation catalog is a derivative asset generated by the compiler.
Building a Modern, Automated Translation Pipeline
If we want to build a world-class developer experience (DX), we should never ask developers to copy-paste translations or wait on translations to ship code. We can design a continuous localization pipeline that leverages GitHub Actions, AST extraction tools, and LLMs or Translation Management Systems (TMS) APIs.
The Architecture
Here is how the data flows in a modern content engineering architecture:
[Developer writes code with <Trans> tags]
│
▼
[Local git commit / PR opened]
│
▼
[GitHub Action triggers CLI tool] ───► Extract new strings to JSON
│
▼
[Push new strings to Localization Platform (e.g., Lingo.dev, Tolgee, Phrase)]
│
▼
[AI / Human Translators localize the keys]
│
▼
[Automated PR created to merge translations back into Main branch]
Step-by-Step GitHub Action Implementation
Let's write a GitHub Action that automatically extracts updated translation keys whenever code is pushed to the main branch, pushes them to a translation service, and fails the build if there are missing translation keys in production bundles.
# .github/workflows/localization.yml
name: Localization Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
extract-and-sync:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Extract Strings via AST
run: npm run i18n:extract
# This runs our compiler (e.g., lingui extract) which scans the JS/TS files
- name: Check for Uncommitted Changes
id: git-check
run: |
git diff --exit-code src/locales/en/messages.json || echo "changes=true" >> $GITHUB_OUTPUT
# If the developer added new UI strings but forgot to run the extractor locally,
# this step catches it and we can automatically commit the updated templates.
- name: Commit Extracted Templates (if missing)
if: steps:git-check.outputs.changes == 'true'
run: |
git config --global user.name "sysseder-bot"
git config --global user.email "bot@sysseder.com"
git add src/locales/en/messages.json
git commit -m "chore(i18n): auto-extract new localization strings [skip ci]"
git push
- name: Push to Localization Platform API
if: github.ref == 'refs/heads/main'
env:
LOCALIZATION_API_KEY: ${{ secrets.LOCALIZATION_API_KEY }}
run: |
curl -X POST https://api.localizationplatform.com/v1/sync \
-H "Authorization: Bearer $LOCALIZATION_API_KEY" \
-H "Content-Type: application/json" \
-d @src/locales/en/messages.json
The Future: LLMs and Context-Aware Translations
We can't talk about modern developer tools without talking about AI. Traditional machine translation (like basic Google Translate) is notoriously bad at localizing software because it lacks context.
For example, does the word "Book" in an application mean a physical novel (Noun), or does it mean reserving a hotel room (Verb)? To a simple dictionary translation tool, they look identical.
Modern content engineering platforms are leveraging LLMs (like GPT-4o or Claude 3.5 Sonnet) to perform context-aware translation. By passing the AST-extracted keys along with metadata—such as surrounding code snippets, developer comments, or screenshot visual assets—the LLM can provide incredibly accurate translations on the first try.
Here is an example of a prompt format that content engineering pipelines use to query translation LLMs:
System: You are an expert software localization engine. Translate the provided JSON strings.
Maintain all interpolation variables (e.g., {count}, {name}) exactly as they are written.
Context:
- This application is a SaaS Dashboard for cloud infrastructure.
- The target audience is DevOps engineers.
JSON Input:
{
"billing.upgrade_prompt": {
"message": "Click here to book more nodes for your cluster.",
"developer_notes": "The word 'book' here is a verb meaning 'to reserve' or 'to purchase' server instances."
}
}
Translate to: [German, Spanish, Japanese]
By treating localization as a data compilation and enrichment problem, we drastically reduce the feedback loop of shipping global software.
Conclusion
The days of manually editing massive, untyped en.json files and waiting weeks for manual translators to return spreadsheets are over. The rise of roles like "Content Engineer" highlights a broader shift in our industry: we are treating copy and localization as code, subject to the same rigorous standards of compilation, static analysis, type-safety, and CI/CD automation that we apply to our databases and APIs.
If you're starting a new project, take a look at modern tools like LinguiJS, Paraglide, or platforms like Lingo.dev. Stop writing manual translation keys, embrace AST-driven extraction, and let your pipelines do the heavy lifting.
What does your localization stack look like? Are you still wrestling with giant JSON files, or have you migrated to a compiler-based approach? Let me know in the comments below, or hit me up on Twitter/X at sysseder.com!
Until next time, happy coding!