DEV Community

Cover image for I Built an AI Agent That Makes Any Landing Page (Next.js App) Multilingual in Minutes
Kashif Rezwi
Kashif Rezwi

Posted on

I Built an AI Agent That Makes Any Landing Page (Next.js App) Multilingual in Minutes

The Problem Nobody Wants to Solve

Going multilingual is one of the most commonly requested — and most commonly abandoned — features in software development.

Companies know they need it. Users demand it. Global markets require it. Revenue depends on it. But the implementation consistently defeats engineering teams.

Why? Because translation is only 10% of the work. The other 90% is orchestration:

  • String externalization — wrapping every hardcoded string in t() functions across hundreds of files
  • Configuration — setting up i18n libraries, locale directories, provider wrappers, config files
  • Translation management — maintaining JSON locale files that drift out of sync with every feature
  • Deployment — creating branches, opening PRs, setting up preview environments to verify the result
  • Coordination — developers, translators, reviewers, and PMs rarely speaking the same language about the problem

Retrofitting i18n into an existing codebase costs 3–10x more than building it in from the start. So teams punt. And the feature dies on the backlog.

AI tools like Lingo.dev have dramatically lowered the cost of the translation step itself. But they still require a developer to sit down, read the documentation, configure the tooling, run the CLI, manage the output, and set up a preview. That's still hours of focused engineering time per project.

The real problem isn't translation. Translation is solved. The problem is the orchestration gap — the hours between "we want multilingual support" and "here's a working branch with a live preview."

That's the gap LingoAgent fills.


What LingoAgent Does

One input. One click. One working multilingual branch with a live preview.

You provide a GitHub repository URL and select your target languages. An autonomous AI agent takes over completely:

  1. Clones the repo into an isolated cloud sandbox
  2. Detects the framework (Next.js App Router)
  3. Analyzes the codebase for existing i18n setups
  4. Configures the i18n scaffolding (providers, switcher, components)
  5. Extracts every hardcoded JSX string via Babel AST and translates them with Lingo.dev SDK
  6. Commits everything to a new branch and opens a GitHub PR
  7. Triggers a live Vercel preview deployment

The entire process takes few minutes instead of days. The developer's only job is to review the PR.

🌐 Live: lingo-agent.vercel.app

📦 Repo: github.com/Kashif-Rezwi/lingo-agent


The Approach: Orchestration, Not Translation

This distinction drove every design decision.

Lingo.dev has already solved the hard problem of accurate, context-aware AI translation. LingoAgent doesn't compete with that — it wraps Lingo.dev in an intelligent agent pipeline that removes every remaining manual step.

What Lingo.dev provides (5 tools)

Tool What It Does
Compiler Build-time AST translation for React — zero code changes
CLI Translates project files across 26 formats via i18n.json config
CI/CD Action GitHub Action that automates CLI on every push
SDK Runtime translation in 7+ languages for dynamic content
MCP Server Exposes framework-specific setup knowledge to AI assistants

What LingoAgent adds on top

What Lingo.dev Still Needs a Human For What LingoAgent Does Instead
Reading and understanding the docs Agent queries the MCP server for exact instructions
Cloning the repo locally Agent clones into an isolated E2B sandbox
Detecting framework & choosing setup path Agent reads package.json and config files
Modifying next.config.ts and layout.tsx Agent applies verified changes from MCP
Writing i18n.json configuration Agent generates it from user's selected locales
Running npm install and translation engine Agent executes inside the sandbox
Creating branch and opening PR Agent calls GitHub API via Octokit
Setting up preview deployment Agent triggers Vercel and polls until ready

LingoAgent uses the Lingo.dev SDK for string translation and the MCP Server for correct i18n scaffolding, building an autonomous pipeline around them.


Architecture: Three Layers

The system has three distinct layers:

┌──────────────────────────────────────────────────────┐
│                   Browser (User)                     │
│                                                      │
│  Next.js 14 App Router (Client)                      │
│  ┌──────────┬────────────┬────────────────────────┐  │
│  │  /login  │ /dashboard │ /jobs/[jobId]          │  │
│  │  GitHub  │ New Job /  │ SSE log stream         │  │
│  │  OAuth   │ History /  │ + PR / Preview links   │  │
│  │          │ Settings   │                        │  │
│  └──────────┴────────────┴────────────────────────┘  │
└──────────────────────┬───────────────────────────────┘
                       │ REST + SSE
                       ▼
┌──────────────────────────────────────────────────────┐
│           NestJS API Server (Backend)                │
│                                                      │
│  AuthGuard → AgentController → AgentService          │
│  JobsService (Prisma + Neon PostgreSQL)              │
│                                                      │
│  ┌────────────────────────────────────────────────┐  │
│  │          Agent Pipeline (per job)              │  │
│  │  Groq LLM — tool call planner                  │  │
│  │  7 sequential tools in E2B sandbox             │  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
│  External Services:                                  │
│  ├── GitHub (Octokit) — clone / branch / PR          │
│  ├── E2B — isolated sandbox execution                │
│  ├── Lingo.dev SDK + MCP — translation               │
│  └── Vercel API — preview deployment                 │
└──────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Why these choices?

Decision Rationale
Monorepo (/client + /server) Clear separation; each deploys independently
SSE over WebSockets One-way log streaming is all we need; SSE is simpler and HTTP-native
E2B sandboxes Complete process isolation — git, npm, node all run in throwaway VMs. No untrusted code on our servers
Sequential tool forcing Prevents the LLM from skipping steps or calling tools out of order
RxJS ReplaySubject per job Late-joining SSE connections replay all past events from job start
Groq (Llama 3.3 70B) Fast inference, free tier available, excellent tool-calling accuracy

The 7-Step Agent Pipeline

This is the core of LingoAgent. Each job runs a strictly sequential 7-step pipeline. The LLM is only ever shown one tool schema at a time (toolChoice: 'required'), forcing it to call that exact tool. The server executes the tool manually — the LLM only plans, never executes.

Step 1: clone_repo

Spins up a fresh E2B cloud sandbox and clones the repository into /workspace. Returns a sandboxId that every subsequent tool uses.

Step 2: detect_framework

Reads package.json, next.config.ts, and directory structure. Identifies Next.js version, App Router vs Pages Router, and the layout file path. If it's not Next.js App Router, the pipeline halts immediately with a clear error — no wasted time.

Step 3: analyze_repo

Scans for existing i18n libraries (next-intl, i18next, react-i18next). If found, the agent stops and warns the user about potential conflicts. Counts JSX files to estimate the translation workload.

Step 4: setup_lingo

Queries the Lingo.dev MCP server for exact setup instructions for the detected framework. Then:

  • Writes i18n.json configuration
  • Creates the TextTranslator component, LingoProvider, and LanguageSwitcher
  • Patches layout.tsx to wrap the app with the provider

This step is where the MCP server shines — instead of the LLM hallucinating configuration, it gets verified, up-to-date instructions directly from Lingo.dev.

Step 5: install_and_translate

The heaviest step:

  1. Runs npm install inside the sandbox
  2. Uses Babel AST parsing to extract every hardcoded JSX string — text nodes, placeholder, title, alt, aria-label attributes
  3. Translates extracted strings in chunks via the Lingo.dev SDK
  4. Writes locale JSON files to public/locales/{lang}.json

If the Lingo.dev API key is invalid or quota is exceeded, the pipeline aborts immediately with actionable guidance pointing the user to the Settings tab.

Step 6: commit_and_push

Uses Octokit to:

  1. Create a new branch (lingo/add-multilingual-support)
  2. Commit all modified and new files
  3. Open a pull request with a structured description

The PR body includes what was translated, which files were modified, and known limitations.

Step 7: trigger_preview

Calls the Vercel API to trigger a preview deployment for the new branch. Polls deployment status every few seconds until it's ready, then returns the live preview URL.

The SSE stream emits { type: 'complete', data: { prUrl, previewUrl } } — and the frontend displays both links.


How the LLM Orchestration Actually Works

This is the part I found most interesting to build. The pattern is LLM as planner, not executor.

// Simplified version of the core loop
const result = await generateText({
  model: groq('llama-3.3-70b-versatile'),
  system: SYSTEM_PROMPT,
  messages: conversationHistory,
  tools: { [currentTool.name]: currentTool.schema },
  toolChoice: 'required',  // Force the LLM to call THIS tool
  maxSteps: 1,
});
Enter fullscreen mode Exit fullscreen mode

Key design decisions:

  1. One tool at a time. The LLM never sees all 7 tools simultaneously. It sees exactly one tool schema and is forced to produce arguments for it. This eliminates tool selection errors.

  2. Server-side execution. The LLM returns tool call arguments. The server validates them, executes the tool, and feeds the result back into the conversation history. The LLM never touches the actual APIs.

  3. Up to 3 retries. If the LLM produces invalid arguments, the server appends a correction prompt and retries. After 3 failures, the pipeline aborts.

  4. Context window as state. Each tool's return value feeds the next through the LLM's conversation history. The sandboxId from step 1 flows through steps 2–7. The framework from step 2 informs step 4. The branchName from step 6 feeds step 7.

This architecture avoids the two biggest pitfalls of LLM agent systems:

  • Hallucination — the LLM can't hallucinate a tool call because it's only shown one option
  • SDK timeouts — the server controls execution timing, not the LLM

Real-Time Observability

Users can tolerate a minute process. They cannot tolerate a minute black box.

Every tool emits structured log events through an SSE stream:

// Each tool emits logs via an emit function
emit({ type: 'log', message: '📦 Cloning repository...', status: 'info' });
emit({ type: 'progress', step: 'clone_repo', percent: 15 });
// ... work happens ...
emit({ type: 'log', message: '✅ Repository cloned successfully', status: 'success' });
Enter fullscreen mode Exit fullscreen mode

On the frontend, the /jobs/[jobId] page opens an EventSource connection and renders logs in real time. Because the backend uses an RxJS ReplaySubject, even if the user navigates away and comes back, they see the full log history from the start.


Auth Flow: GitHub Token as Bearer

LingoAgent uses GitHub OAuth exclusively — no passwords, no separate accounts.

The clever part: the same GitHub access token that authenticates the user on the frontend is forwarded as a Bearer token to the NestJS backend. The backend:

  1. Validates the token exists
  2. Uses it directly to call GitHub APIs (clone, branch, commit, PR) on behalf of the user

No separate JWT system. No session store on the backend. The user's own GitHub permissions govern what repositories the agent can access.


Custom API Keys: Bypassing Free Tier Limits

One practical challenge: shared API keys hit rate limits fast during a hackathon demo.

LingoAgent lets users supply their own Lingo.dev and Groq API keys via Dashboard → Settings. These keys are:

  • Stored in localStorage (never sent to any third party)
  • Sent with each job request as optional headers
  • Used by the backend to override server-side defaults

This means the demo works even when the shared keys are exhausted — users just paste in their own free-tier keys.


Tech Stack

Frontend

Tech Purpose
Next.js 14 (App Router) React framework
NextAuth.js GitHub OAuth
Tailwind CSS Styling
TypeScript Type safety

Backend

Tech Purpose
NestJS 11 API server, DI
Vercel AI SDK generateText + tool calling
Groq (Llama 3.3 70B) LLM inference
E2B Isolated sandbox VMs
Lingo.dev SDK String translation
Lingo.dev MCP Setup instructions
Octokit GitHub REST API
Prisma + Neon PostgreSQL Job storage
RxJS SSE stream management

Three Principles That Guided Every Decision

1. Reliability over breadth

A demo that works perfectly for Next.js App Router is worth more than a demo that claims to support five frameworks but breaks on all of them.

Lingo.dev provides its deepest support for Next.js App Router. Its MCP server, compiler, and SDK are all tuned for this stack. Supporting Vite, Remix, or Pages Router would each require separate detection logic, different patching strategies, and independent testing — multiplying the surface area several times.

In a hackathon, doing one thing flawlessly beats doing five things poorly.

2. Observable beats fast

The live log stream isn't just a feature — it's core to the product experience. Watching the agent clone, detect, configure, translate, commit, and deploy in real time builds trust and makes the few minute wait feel productive, not idle.

3. LLM as planner, not executor

The Groq LLM decides what to do (which arguments to pass). The NestJS server decides how to do it (actual API calls, file operations, error handling). This separation prevents hallucination, avoids SDK timeout issues, and makes the pipeline deterministic and debuggable.


Known Limitations (Honest Assessment)

These are deliberate scope constraints, not bugs:

  • Next.js App Router only — Pages Router, Vite, Remix not supported
  • Hardcoded strings in JS logic are not translated — Babel AST targets JSX text nodes and common attributes (placeholder, title, alt, aria-label). Strings in variables or API responses may be missed
  • Large repos may time out — E2B sandboxes have a configurable timeout (default 10 min)
  • No monorepo support — single-app repositories only
  • Existing i18n setups may conflict — repos already using next-intl or i18next may produce conflicts

What I Learned

  1. Prisma 7 is a breaking change. The url field in schema.prisma is no longer supported — datasource configuration moved to prisma.config.ts. This caused real deployment pain that taught me to always check for major version breaking changes.

  2. LLM tool-calling is fragile unless you constrain it. Showing an LLM 7 tools and hoping it calls them in order is a recipe for chaos. Showing it one tool at a time with toolChoice: 'required' produces deterministic, reliable behavior.

  3. SSE is dramatically simpler than WebSockets for one-way streaming. No connection management, no heartbeats, no reconnection protocol — just text/event-stream and you're done.

  4. E2B sandboxes are a superpower for agent systems. Running untrusted code? npm install with unknown dependencies? Git clone with arbitrary URLs? Put it all in a throwaway VM and don't think twice.

  5. The MCP protocol is underrated. Instead of the LLM hallucinating framework configuration, it gets verified instructions from Lingo.dev's own MCP server. This single integration eliminated an entire class of bugs.


Checkout the Video

Demo Video: Google Drive link

Try It Yourself

Demo repo: Kashif-Rezwi/lingo-agent-demo-app — a clean Next.js 14 App Router landing page built for testing.

  1. Sign into LingoAgent with GitHub
  2. Paste https://github.com/Kashif-Rezwi/lingo-agent-demo-app
  3. Select Japanese, French, and Arabic
  4. Click Start and watch the agent work
  5. Review the GitHub PR and live Vercel preview

Built by Kashif Rezwi for the Lingo.dev Hackathon 2026.

Top comments (0)