Skip to main content
Tutorial

Deploying a Bilingual Next.js App to Vercel: Lessons from Real Production

4 min read

METADATA:{"excerpt":"A bilingual Next.js project on Vercel taught me the hard way about i18n configuration, caching gotchas, and UAE client expectations.",…

METADATA:{"excerpt":"A bilingual Next.js project on Vercel taught me the hard way about i18n configuration, caching gotchas, and UAE client expectations.","tags":["Next.js","Vercel","i18n","TypeScript","Firebase"]}

Let me take you back to 2 a.m. last March. I was deploying a bilingual Next.js app for a UAE real estate client. The staging environment worked perfectly. But on Vercel's production deployment? Arabic paths were returning 404s. English worked. The app is supposed to handle both.

I stared at the logs for an hour. Then remembered a line in the client's wireframe: "Header navigation must show current language first." That tiny detail forced deeper i18n integration than I'd planned.


The Setup: When a Client Actually Wants Both Languages

We were building an Expo web app (part of Greeny Corner's desktop version) where users in Abu Dhabi could view plant care guides in Arabic or English. No fallbacks – if your browser didn't match, you got a prompt.

Used next-i18next with locales ['en', 'ar-AE']. Backend translations were served via Firebase Realtime Database, while component translations lived in static JSON files. SSR meant rendering proper lang attributes and dir="rtl" for Arabic.


i18n Config That Actually Works

Here's what got me burned: Next.js's built-in i18n doesn't handle dynamic routes well with Vercel's edge caching.

js
// next.config.js  
i18n: {  
  locales: ['en', 'ar-AE'],  
  defaultLocale: 'en',  
  domains: [  
    { domain: 'greenycorner.ae', defaultLocale: 'en' },  
    { domain: 'ar.greenycorner.ae', defaultLocale: 'ar-AE' }  
  ]  
}  

Had to create custom server headers in vercel.json:

json
"headers": [  
  {  
    "source": "/:path*",  
    "headers": [  
      { "key": "Cache-Control", "value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=3600" },  
      { "key": "Vary", "value": "Accept-Language"}  
    ]  
  }  
]  

This let Vercel's CDN cache both language versions separately.


Deployment Drama: When Your Router Becomes a Detective

First deploy: /ar/care-guide/1 worked locally but failed on Vercel. Turned out my dynamic revalidate() function wasn't serializing locale paths correctly.

Had to modify this on the API side:

ts
// pages/api/care-guide/[id].ts  
const locale = req.headers['accept-language']?.includes('ar-AE') ? 'ar-AE' : 'en'  
const content = await getTranslation(locale, `care_guides_${id}`)  
res.json(content)  

But client-side navigation via next/router needed the same treatment. Wasted half a day syncing server/client locale detection logic.


Build Time Monsters Aren't Mythical

We added two locales – build time quadrupled to 14 minutes. Vercel's 200 OK responses felt sarcastic after I hit the timeout limit thrice. Found a hack:

For non-critical pages, replaced getStaticProps with client-side fetches using Suspense. Added this script to reduce page regeneration:

js
// scripts/prune-build.js  
const fs = require('fs')  
if (process.env.NODE_ENV === 'production') {  
  fs.readdirSync('public/locales/ar-AE')  
    .filter(file => new Date(file.modified) < someThreshold)  
    .forEach(fs.unlinkSync)  
}  

UAE Clients: No "WIP" for Core Features

Here's the thing about UAE businesses: they won't accept "we'll fix language bugs in v2". When I suggested delaying the Arabic launch for Tawasul Limo's booking platform, the client's PM showed me last month's bounce rate stats from Google Analytics – 59% from Arabic speakers hitting English pages.


The Cookie Problem I Almost Missed

We stored preferred language in a cookie. Great idea… until Vercel cached the first response from a bot with no cookie set. Suddenly the default locale was broken.

Final solution:

  1. Detect locale from x-vercel-ip-country header (Vercel's geolocation)
  2. Set Set-Cookie header only after first interaction
  3. Use a ?lang=en query param as fallback
  4. Add tags everywhere

Postmortem

Would I change anything?

Yes. For my next deployment to handle Reach Home Properties' mortgage calculator, I'll:

  1. Test domain routing in a Vercel preview environment before final build
  2. Use Cloudinary for translated images (spent 3 hours debugging RTL layouts for SVGs)
  3. Prioritize Firebase Cloud Functions for translation APIs with region: 'me-west1'

If you're deploying a bilingual Next.js app this year: check your cache headers twice, expect 2x build times, and never assume locale detection works across serverless splits.

Got questions? Hit me up on sarahprofile.com/contact. I'm drinking tea while watching cricket scores, and I swear deploying Next apps gets easier with every project… but only after the third 2 a.m. caffeine crash.

S

Sarah

Senior Full-Stack Developer & PMP-Certified Project Lead — Abu Dhabi, UAE

7+ years building web applications for UAE & GCC businesses. Specialising in Laravel, Next.js, and Arabic RTL development.

Work with Sarah