Article · 2026-01-28

Adding Multi-Language AI Translation to My Blog

After implementing the RAG-powered AI assistant, I wanted to make my blog content accessible to a global audience. Rather than manually translating each post, I built an on-demand AI translation system that supports 10 languages—automatically preserving Markdown formatting, caching translations to minimize API costs, and showing loading feedback during requests.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     Browser (Frontend)                          │
├─────────────────────────────────────────────────────────────────┤
│  Language Selector ──▶ Check localStorage cache                 │
│         │                     │                                 │
│         │◀── Cache hit ───────┘                                 │
│         │                                                       │
│         ▼ Cache miss                                            │
│  POST /api/translate { content, targetLang, slug }              │
└─────────┼───────────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────────┐
│              Cloudflare Worker (/api/translate)                 │
│  ┌─────────────────┐         ┌─────────────────┐                │
│  │  Check KV Cache │───hit──▶│  Return cached  │                │
│  └────────┬────────┘         └─────────────────┘                │
│           │ miss                                                │
│           ▼                                                     │
│  ┌─────────────────┐        ┌─────────────────┐                 │
│  │ Call OpenAI API │───────▶│ Cache in KV     │                 │
│  │  (gpt-4o-mini)  │        │ (30 days TTL)   │                 │
│  └─────────────────┘        └─────────────────┘                 │
└─────────────────────────────────────────────────────────────────┘

Supported Languages

The system supports 10 languages covering most of the world's population:

Code Language Code Language
en English fr Français
zh 中文 de Deutsch
ja 日本語 pt Português
ko 한국어 ru Русский
es Español ar العربية

Implementation

1. Language Selector UI

A clean dropdown that appears on every blog post:

<div class="lang-selector" id="lang-selector">
    <span class="lang-selector-label">
        <svg><!-- translate icon --></svg>
        Language
    </span>
    <select class="lang-select" id="lang-select">
        <option value="en">🇺🇸 English</option>
        <option value="zh">🇨🇳 中文</option>
        <option value="ja">🇯🇵 日本語</option>
        <!-- ... more languages -->
    </select>
    <button class="lang-original-btn" id="lang-original-btn">
        Show Original
    </button>
    <span class="lang-status" id="lang-status"></span>
</div>

2. Loading Animation

A CSS-only spinner that shows during translation:

.lang-status.translating {
    color: var(--vermilion);
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.lang-status.translating::before {
    content: '';
    width: 14px;
    height: 14px;
    border: 2px solid #f0f0f0;
    border-top-color: var(--vermilion);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

3. Dual-Layer Caching Strategy

To minimize API costs, I implemented caching at two levels:

Frontend (localStorage) - 7 day TTL:

function getTranslationCache(key) {
    const raw = localStorage.getItem(key);
    if (!raw) return null;

    const data = JSON.parse(raw);
    if (Date.now() - data.timestamp > 7 * 24 * 60 * 60 * 1000) {
        localStorage.removeItem(key);
        return null;
    }
    return data.content;
}

Backend (Cloudflare KV) - 30 day TTL:

// Check cache
const cached = await env.TRANSLATIONS.get(cacheKey);
if (cached) {
    return Response.json({ translated: cached, cached: true });
}

// After translation, cache result
await env.TRANSLATIONS.put(cacheKey, translated, {
    expirationTtl: 30 * 24 * 60 * 60
});

4. Translation API (Cloudflare Worker)

The worker handles translation requests with careful prompt engineering:

if (path === "/api/translate") {
    const { content, targetLang, slug } = await request.json();

    const response = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: {
            "Authorization": `Bearer ${env.OPENAI_API_KEY}`,
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            model: "gpt-4o-mini",
            messages: [{
                role: "system",
                content: `You are a professional translator. Translate to ${langName}.

Rules:
- Preserve all Markdown formatting
- Keep code blocks unchanged
- Keep technical terms in original form when appropriate
- Do not add explanations
- Return only the translated content`
            }, {
                role: "user",
                content
            }],
            max_tokens: 4096,
            temperature: 0.3,
        }),
    });

    const data = await response.json();
    return Response.json({ translated: data.choices[0].message.content });
}

5. Smart Language Detection

The frontend auto-detects the original language to set the correct default:

function detectLanguage(text) {
    const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
    const totalChars = text.length;
    if (chineseChars / totalChars > 0.3) return 'zh';
    return 'en';
}

Key Design Decisions

Translate Markdown, not HTML — When available, I translate the raw Markdown source and re-render it. This preserves formatting better than translating rendered HTML.

Low temperature (0.3) — Translation requires consistency, not creativity. A low temperature produces more reliable results.

Content hash for cache keys — Using slug_hash_lang as the cache key ensures that if the original content changes, the translation cache is automatically invalidated.

Optional KV binding — The worker operates without KV storage if needed; it simply lacks server-side caching. This simplifies development and reduces deployment constraints.

Cost Analysis

Using gpt-4o-mini, a typical blog post requires approximately 3000 input tokens and 3000 output tokens, costing about $0.003 per translation. With dual-layer caching, repeated requests incur no API cost. A blog with moderate traffic should expect under $5 per month in translation costs.

Next Steps

Future improvements under consideration:

The complete implementation is in my website repository.

© 2026 Yuxu Ge ·