Back to tstlai
Case StudyLive Integration

How ZaguanLabs Uses tstlai

A real-world case study of tstlai in production. This very website you're reading is powered by tstlai for automatic AI translation. Here's exactly how we built it.

Try it now: Visit /es, /fr, /de, /ja, or try /ar for RTL Arabic — tstlai automatically sets dir="rtl"!

The Challenge

ZaguanLabs needed to support multiple languages across their entire marketing site without:

  • Maintaining separate JSON translation files for each language
  • Refactoring every component to use translation keys
  • Waiting for manual translations before launching new content
  • Dealing with stale translations when English content changes

The solution? Use tstlai to automatically translate the entire site on-the-fly, with intelligent caching to keep costs low and performance high.

Architecture Overview

Our integration uses four key files that work together:

src/lib/translator.tsTranslator factory

Creates and caches Tstlai instances per locale with Redis caching and brand term exclusions.

src/proxy.tsMiddleware

Handles locale detection from browser headers and URL routing using tstlai's language support API.

src/app/[locale]/layout.tsxLayout wrapper

Injects the AutoTranslate component for non-English locales with streaming enabled.

src/lib/session-token.tsSecurity tokens

Generates cryptographic session tokens for secure, dynamic API endpoints.

src/app/api/[sessionToken]/*/route.tsDynamic API routes

Translation endpoints with session-based paths that prevent unauthorized access.

Implementation

1
Translator Factory

We create a singleton factory that returns cached Tstlai instances per locale. This ensures we don't create multiple instances for the same language.

// src/lib/translator.ts
import 'server-only';
import { Tstlai } from 'tstlai';

const instances = new Map<string, Tstlai>();

export function getTranslator(locale: string = 'en') {
  if (!instances.has(locale)) {
    instances.set(
      locale,
      new Tstlai({
        targetLang: locale,
        sourceLang: 'en',
        provider: {
          type: 'openai',
          apiKey: process.env.OPENAI_API_KEY,
          model: 'zaguanai/gemini-flash-latest',  // Google Gemini Flash - fast and efficient for translation
          baseUrl: 'https://api.zaguanai.com/v1',
        },
        cache: {
          type: process.env.REDIS_URL ? 'redis' : 'memory',
          connectionString: process.env.REDIS_URL,
          keyPrefix: 'zaguanlabs:',
        },
        // Keep brand names untranslated
        excludedTerms: [
          'Zaguan', 'ZaguanLabs', 'Zaguán AI', 
          'tstlai', 'chaTTY', 'RayLM', 'LuminaryChat'
        ],
        translationContext: 'Tech startup website for AI developer tools and SDKs',
      }),
    );
  }
  return instances.get(locale)!;
}

Key insight: We use Google's Gemini 2.5 Flash model via the Zaguán OpenAI-compatible API (accessible at https://api.zaguanai.com/v1). This model supports many languages with excellent quality and low latency for fast response times, making it ideal for real-time website translation with high accuracy and consistency across all content.

Brand protection: The excludedTerms option ensures brand names like "ZaguanLabs" and "tstlai" are never translated, maintaining brand consistency across all languages.

2
Locale Detection Middleware

We use tstlai's exported language support utilities to validate locales and handle browser language negotiation.

// src/proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { isLanguageSupported, SHORT_CODE_DEFAULTS } from 'tstlai';

// Use tstlai's short codes for browser negotiation
const NEGOTIATION_LOCALES = Object.keys(SHORT_CODE_DEFAULTS);
export const DEFAULT_LOCALE = 'en';

function getLocale(request: NextRequest): string {
  const headers = { 
    'accept-language': request.headers.get('accept-language') || '' 
  };
  const languages = new Negotiator({ headers }).languages();
  return match(languages, NEGOTIATION_LOCALES, DEFAULT_LOCALE);
}

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const segments = pathname.split('/');
  const potentialLocale = segments[1];
  
  // Validate locale using tstlai's language support
  if (potentialLocale && /^[a-z]{2}$/.test(potentialLocale)) {
    if (isLanguageSupported(potentialLocale)) {
      return; // Valid locale, continue
    }
    // Unsupported language - redirect to English
    segments[1] = 'en';
    return NextResponse.redirect(new URL(segments.join('/'), request.url));
  }

  // No locale in URL, detect from browser
  const locale = getLocale(request);
  return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}

3
Layout Integration (The Magic)

This is where the magic happens. We add a single component to our locale layout, and the entire app translates automatically. No changes to any page components needed.

// src/app/[locale]/layout.tsx
import { AutoTranslate } from 'tstlai/next';
import { getTranslator } from '@/lib/translator';
import { generateSessionToken } from '@/lib/session-token';

interface LocaleLayoutProps {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}

export default async function LocaleLayout({ 
  children, 
  params 
}: LocaleLayoutProps) {
  const { locale } = await params;
  const translator = getTranslator(locale);
  const dir = translator.getDir(); // 'ltr' or 'rtl'

  // Generate a unique session token for secure API access
  const sessionToken = locale !== 'en' ? generateSessionToken() : null;

  return (
    <div dir={dir} lang={locale}>
      {children}
      {/* AutoTranslate with dynamic, secure endpoints */}
      {locale !== 'en' && sessionToken && (
        <AutoTranslate 
          targetLang={locale} 
          stream={true}
          endpoint={`/api/${sessionToken}/translate`}
          streamEndpoint={`/api/${sessionToken}/stream`}
          streamBuffer={500}
        />
      )}
    </div>
  );
}

Secure by default! Each page render generates a unique session token that becomes part of the API path. This prevents attackers from guessing or abusing your translation endpoints. The AutoTranslate component scans the DOM, extracts text nodes, and streams translations back. The getDir() method automatically returns 'rtl' for Arabic, Hebrew, Persian, and other RTL languages.

4
Dynamic API Routes

We use Next.js dynamic routes with session tokens in the path. Each request validates the token before processing, preventing unauthorized access to the translation API.

// src/app/api/[sessionToken]/stream/route.ts
import { createNextStreamingRouteHandler } from 'tstlai/next';
import { getTranslator } from '@/lib/translator';
import { validateSessionToken } from '@/lib/session-token';

interface RouteParams {
  params: Promise<{ sessionToken: string }>;
}

export const POST = async (req: Request, { params }: RouteParams) => {
  const { sessionToken } = await params;

  // Validate session token before processing
  if (!validateSessionToken(sessionToken)) {
    return new Response(JSON.stringify({ error: 'Invalid session' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  const body = await req.json();
  const { targetLang } = body;
  
  const translator = getTranslator(targetLang || 'en');
  const handler = createNextStreamingRouteHandler(translator);
  
  return handler(new Request(req.url, {
    method: 'POST',
    headers: req.headers,
    body: JSON.stringify(body),
  }));
};

Why dynamic routes? Static endpoints like /api/translate can be discovered and abused. With session tokens in the path, attackers would need to guess a cryptographically random string to access the API.

Results

46
Languages supported instantly
0
JSON translation files to maintain
~50ms
Cache hit latency (Redis)
5
Files to integrate tstlai

Lessons Learned

Use streaming for better UX

The streaming endpoint with a 500ms buffer provides a much smoother experience than waiting for all translations to complete. Users see content appearing progressively.

Redis caching is essential for production

Memory caching works for development, but Redis ensures translations persist across deployments and server restarts. Most page loads hit the cache with ~50ms latency.

Exclude brand terms upfront

Adding brand names to excludedTerms from the start prevents awkward translations like "ZaguanLabs" becoming "LaboratoriosZaguan".

Translation context improves quality

Setting translationContext to describe your site helps the AI make better word choices. "Tech startup website" produces different translations than "children's educational content".

Secure your endpoints from day one

Public translation APIs can be abused to run up your AI costs. Using session-based dynamic routes means attackers can't discover or guess your endpoints. Each page render gets a unique, cryptographically secure path.

Ready to Add AI Translation to Your App?

Get started with tstlai in minutes. No JSON files, no manual translations.