Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungWhy Claude Code keeps writing shell commands that fail on your Mac(20.09.2026 um 21:06 Uhr)
Sichere Programmierungllms.txt v2: What the Spec Says, and What 137,000 Domains Show(20.09.2026 um 21:17 Uhr)
Sicherheitslücken (CVE)NiceTryGPT: Less pattern matching. More actual hacking.(20.09.2026 um 21:19 Uhr)
IT Security VideoActivities BoF (kde2026)(20.09.2026 um 00:00 Uhr)
IT Security Toolsirdoc-app(20.09.2026 um 20:33 Uhr)
Sichere ProgrammierungWhy Claude Code keeps writing shell commands that fail on your Mac(20.09.2026 um 21:06 Uhr)
Sichere Programmierungllms.txt v2: What the Spec Says, and What 137,000 Domains Show(20.09.2026 um 21:17 Uhr)
Sicherheitslücken (CVE)NiceTryGPT: Less pattern matching. More actual hacking.(20.09.2026 um 21:19 Uhr)
IT Security VideoActivities BoF (kde2026)(20.09.2026 um 00:00 Uhr)
IT Security Toolsirdoc-app(20.09.2026 um 20:33 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Multilingual SaaS Architecture: Serving 7 Languages on Vercel Edge

Reagiere als Erste:r — dein Feedback zählt!

Serving multiple languages well is mostly about three things: correct URLs, correct hreflang, and a build pipeline that never silently ships English to a Japanese user. This post is the architecture I run for a 7-language SaaS on Vercel — including the mistakes that taught me each rule.

The seven URL patterns I considered (and the one I picked)

Pattern Example Pro Con
Subdomain ja.korlens.app/about Clean separation DNS, SSL per language
Subdirectory korlens.app/ja/about One project, easy crawl Routing logic in middleware
Query string korlens.app/about?lang=ja Trivial Bad for SEO, Google ignores
Accept-Language only korlens.app/about One URL Breaks share links, bad for SEO

Subdirectory wins for indie scale. One Vercel project, one analytics view, one sitemap. Routes look like:

/                   → English (default, no prefix)
/ko/...             → Korean
/ja/...             → Japanese
/zh/...             → Simplified Chinese
/es/...             → Spanish
/fr/...             → French
/de/...             → German

The decision that took me two rewrites: the default language has no prefix. /about is English; /ja/about is Japanese. Search engines treat / as the canonical English page; /ja/about declares its alternates via hreflang. If you prefix every language equally (/en/about), you have to deal with two canonical English URLs and a / that 308s to /en/. It works but it's noisy.

Edge middleware: 30 lines, runs on every request

// middleware.ts (Next.js, runs on Vercel Edge)
import { NextResponse, type NextRequest } from "next/server";

const LOCALES = ["ko", "ja", "zh", "es", "fr", "de"]; // 'en' is default, no prefix
const PUBLIC_FILE = /\.(.*)$/;

export function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl;

  // Skip static files and API
  if (
    pathname.startsWith("/_next") ||
    pathname.startsWith("/api") ||
    PUBLIC_FILE.test(pathname)
  ) {
    return NextResponse.next();
  }

  // First-visit redirect from Accept-Language (only on bare /)
  if (pathname === "/") {
    const cookie = req.cookies.get("NEXT_LOCALE")?.value;
    const accept = req.headers.get("accept-language") || "";
    const detected = cookie || accept.split(",")[0]?.slice(0, 2);
    if (detected && LOCALES.includes(detected)) {
      return NextResponse.redirect(new URL(`/${detected}`, req.url));
    }
  }

  return NextResponse.next();
}

export const config = { matcher: "/((?!_next|api).*)" };

Two rules I learned the hard way:

  1. Never auto-redirect on pages other than /. If a user shares /ja/pricing, they want Japanese — don't sniff their browser and bounce them to /de/pricing.
  2. Persist the choice in a cookie the first time a user explicitly picks a language. Otherwise Accept-Language flip-flopping (Chrome on iOS sometimes reports en-US,ko;q=0.9) creates loops.

hreflang at build time, not runtime

The single biggest SEO win was generating <link rel="alternate" hreflang="..."> tags at build time, not in middleware. Google needs them in the HTML on first response. Pattern:

// app/[locale]/about/page.tsx
export function generateMetadata({ params }: { params: { locale: string } }) {
  const slug = "/about";
  const alternates: Record<string, string> = { "x-default": `https://korlens.app${slug}` };
  for (const l of ["ko","ja","zh","es","fr","de"]) {
    alternates[l] = `https://korlens.app/${l}${slug}`;
  }
  alternates["en"] = `https://korlens.app${slug}`;

  return {
    title: t(slug, params.locale, "title"),
    alternates: { canonical: `https://korlens.app/${params.locale === "en" ? "" : params.locale + slug}`.replace(/\/$/, slug), languages: alternates },
  };
}

Two gotchas:

  • x-default matters. Without it Google guesses which language to show users in countries you don't target.
  • Self-referential hreflang is required. The Japanese page must list itself as hreflang="ja". Skip it and Search Console will flag "no return tag".

Sitemap: one file per language

I split the sitemap by language because it makes broken-link triage 7× faster:

/sitemap.xml          ← index
/sitemap-en.xml
/sitemap-ko.xml
/sitemap-ja.xml
...

Vercel's edge function generates each on demand from a single pages.json. Total cold build is under 600ms even with 4,000 URLs because the file is read once and reused per locale.

Translation pipeline: source of truth is en.json

The mistake I made in v1 was letting translators edit per-language JSON files directly. Drift was instant — Korean had 412 keys, German had 287, Spanish had 401. Fix:

/messages
   en.json        ← canonical
   ko.json
   ja.json
   ...
/scripts
   check_keys.ts  ← CI: diffs every locale against en.json, exits 1 on mismatch

CI fails any PR where a locale has a missing key OR an extra key. Translators submit changes via a small Sheet → JSON script; English is the only language a developer edits.

For machine pre-translation I run a nightly LLM pass that translates new English keys into the six target languages with this prompt shape:

You are a professional translator for a SaaS UI. Source: en.json key "{key}" = "{value}".
Translate to {target_lang}.
Rules:
- Keep placeholders like {name}, {count} unchanged.
- Prefer noun-form labels for buttons (e.g. "Save" → "保存" not "保存する").
- Output JSON: {"value": "...", "confidence": 0..1}

Anything below confidence 0.8 is queued for human review and falls back to English at runtime. Anything above ships.

Performance: same numbers, seven languages

  • First Contentful Paint p75 across all locales: 0.8 - 1.1s (Vercel Edge, Singapore + Frankfurt regions).
  • TTFB p75: 120 - 180ms regardless of locale (middleware adds < 5ms).
  • Build time: 38s for 7 locales × ~50 routes = 350 generated pages. The trick is generateStaticParams returning all locale × slug pairs so the build parallelizes across CPU cores.

What I'd do differently if starting again

  1. Use next-intl from day one instead of rolling my own message loader. The migration was three days of unnecessary work.
  2. Pick exactly the locales you'll maintain, not "all the ones I might want someday." Each locale is a CI test, a translator review, and a debug surface forever.
  3. Treat Korean and Japanese as separate brand voices, not translations of English. The pages that converted best in /ko/ were rewritten from scratch, not translated.

If you're shipping anything beyond 3 locales, the architecture above scales to 10+ without changes — same middleware, same pages.json, just more JSON files. The hard part is never the routing. It's keeping en.json honest and the hreflang self-referential.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Multilingual SaaS Architecture: Serving 7 Languages on Vercel Edge

Thematisch verwandte Begriffe: Multilingual, SaaS, Architecture, Serving · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-93956 | A flaw has been found in olivier-ls PHP-FTS up to 1.1.2. Affected by thi…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick