Wie ich diesen Blog mit KI übersetze
Ich bin Muttersprachler Spanisch. Ich lebe in den Vereinigten Staaten und verwende daher täglich Englisch. Ich schreibe meine Beiträge auf Englisch, um zu üben und mehr Menschen zu erreichen.
Google Analytics hat mir etwas Wichtiges gezeigt. Etwa 30-40 % meiner Besucher kommen aus Ländern, in denen Englisch nicht die Hauptsprache ist. Das hat mir gezeigt, dass ich Potenzial verschenkt habe. Also beschloss ich, den Blog mit KI zu übersetzen.
Mein Setup
- Wie ich Beiträge speichere: Jeder Blogbeitrag ist eine einfache Datei mit Titel, Datum, Inhalt und anderen Details. Stellen Sie sich das wie eine digitale Rezeptkarte vor.
- Mein Website-Framework: Ich verwende Next.js mit Internationalisierungstools, die die automatische Bearbeitung mehrerer Sprachen unterstützen.
- Die von mir verwendete KI: Googles Gemini KI übernimmt die eigentliche Übersetzungsarbeit. Ich gebe ihr strikte Anweisungen, nur den Textinhalt zu übersetzen und alle technischen Teile (wie Links und Code) unberührt zu lassen.
- Ein Befehl erledigt alles: Ich führe ein einzelnes Skript aus, das meinen englischen Beitrag an Googles Gemini KI sendet, die ihn gleichzeitig in alle 8 Sprachen übersetzt. Das Skript organisiert diese Übersetzungen dann in separate Dateien, aktualisiert alle sprachübergreifenden Links, sodass Leser nahtlos zwischen den Sprachen wechseln können, und überprüft, ob jede Sprachversion vorhanden und vollständig ist.
- Sprachumschaltung: Wenn Leser auf einen Sprachwechsel klicken, werden sie automatisch zur richtigen Version des Beitrags weitergeleitet.
Wichtiger Hinweis: Das Skript heißt
translate-gemini.ts
. Es ist ein generisches Übersetzungsskript, das in jede Sprache übersetzen kann. Es verwendet die Umgebungsvariable
TARGET_LOCALE
, um zu bestimmen, in welche Sprache übersetzt werden soll. Sie benötigen also nur dieses eine Skript, keine separaten Skripte für jede Sprache.
Mein Workflow
- Ich schreibe den Beitrag auf Englisch als
app/[locale]/blog/posts/
. Das Feld
<slug>/index.jsoncontent
ist HTML. Codeblöcke sind eingezäunt. - Ich übersetze mit einem Befehl. Das Modell übersetzt
title
,excerpt
,content
undreadTime
. Metadaten wiedate
,tags
undcategories
bleiben gleich. - Ich halte Slugs synchron mit einer
translatedSlugs
-Zuordnung in jeder Datei, Englisch und Übersetzungen. - Ich validiere, dass jede Sprache existiert und die Slug-Zuordnung vollständig ist.
Warum KI für mich funktioniert
- Befolgt Anweisungen: Die Eingabeaufforderung schützt HTML, Code und Links.
- Kulturadaption: KI passt kulturelle Bezüge und Beispiele an die jeweilige Sprachgruppe an, was ich manuell nicht leisten kann, da ich nicht alle kulturellen Nuancen kenne.
- Schnell und gut: Ein schnelles Modell mit automatischem Fallback.
- Konsistenter Ton: Niedrige Temperatur hält den Stil über die Sprachen hinweg konstant.
Sicherheitsvorkehrungen
- HTML-sichere Übersetzung: Ich sende nur die Werte der JSON-Schlüssel, keine Tags oder Attribute.
- Slug-Disziplin:
translatedSlugs
enthält den Slug füren
,es
,fr
,de
,ru
,nl
,it
undzh
. - Validierung: Ein Befehl prüft, ob jede Sprachdatei existiert und die Slug-Zuordnung vollständig ist.
- Höflich zur API: Ich füge eine kleine Verzögerung zwischen den Aufrufen hinzu.
Übersetzungen und SEO
Viele denken, mehrsprachige SEO besteht nur aus dem Übersetzen von Inhalten. Das ist erst der Anfang. Hier ist, was wirklich zählt:
Die technische Grundlage
- Hreflang-Tags: Diese teilen Suchmaschinen mit, welche Sprachseite welchen Nutzern angezeigt werden soll. Ohne sie könnte Google die falsche Sprache anzeigen oder Ihre Übersetzungen als doppelte Inhalte behandeln.
- URL-Struktur: Jede Sprache benötigt ihren eigenen URL-Pfad. Ich verwende
/es/blog/
,/fr/blog/
usw. Dies hilft Suchmaschinen, die Sprachhierarchie zu verstehen. - Sitemaps: Nehmen Sie alle Sprachversionen mit entsprechenden Sprachbezeichnungen in Ihre Sitemap auf. Suchmaschinen müssen jede Version entdecken und indizieren.
- Canonical-Tags: Verweisen Sie jede Sprachversion auf sich selbst, um Verwirrung darüber zu vermeiden, welche die „Haupt“-Version ist.
Über die Übersetzung hinaus: Kulturadaption
- Keywords in jeder Sprache: Menschen suchen in verschiedenen Sprachen unterschiedlich. „Wie man Programmieren lernt“ wird auf Spanisch zu „cómo aprender programación“, nicht nur eine wörtliche Übersetzung.
- Kultureller Kontext: Einige Konzepte lassen sich nicht direkt übersetzen. Ich verlasse mich auf KI, um Beispiele und Bezüge an jede Zielgruppe anzupassen, da ich nicht alle kulturellen Nuancen jeder Sprache kenne.
- Das Suchvolumen variiert: Einige Themen sind in bestimmten Sprachen beliebter. Ich priorisiere Sprachen, in denen meine Inhalte das größte Potenzial haben.
Signale der Benutzererfahrung
- Spracherkennung: Automatische Erkennung der Benutzersprache anhand der Browsereinstellungen, aber mit Möglichkeit zur Überschreibung.
- Konsistente Navigation: Die Sprachumschaltung muss auf jeder Seite funktionieren und den Kontext des Benutzers beibehalten.
- Ladegeschwindigkeit: Jede Sprachversion sollte so schnell wie das Original laden. Lassen Sie die Übersetzung Ihre Website nicht verlangsamen.
- Mobile Erfahrung: Die Sprachumschaltung muss auf Mobilgeräten gut funktionieren, wo der Platz auf dem Bildschirm begrenzt ist.
Überlegungen zur Content-Strategie
- Nicht alles muss übersetzt werden: Einige Beiträge sind möglicherweise relevanter für englischsprachige Leser. Ich übersetze selektiv, basierend auf dem potenziellen Publikum.
- Aktualisierungssynchronisierung: Wenn ich die englische Version aktualisiere, müssen auch alle Übersetzungen aktualisiert werden. Veraltete Übersetzungen schaden der SEO.
- Qualität vor Quantität: Besser wenige, qualitativ hochwertige Übersetzungen als viele schlecht übersetzte Seiten.
- Lokale Backlinks: Übersetzte Inhalte können Backlinks von Websites in diesen Sprachen erhalten und Ihre allgemeine Domain-Autorität verbessern.
Die wichtigste Erkenntnis: Bei mehrsprachiger SEO geht es darum, separate, optimierte Erlebnisse für jede Sprachgruppe zu schaffen, nicht nur Wörter zu übersetzen.
Die Befehle, die ich ausführe
# Übersetze einen Beitrag in alle Sprachen\nnpm run translate-all translate my-new-post\n\n# Überprüfe, ob alle Sprachdateien und Slug-Zuordnungen vorhanden sind\nnpm run translate-all validate my-new-post\n\n# Übersetze erneut und überschreibe Übersetzungen nach dem Bearbeiten des englischen Textes\nnpm run translate-all translate my-new-post --force
Das ist alles. Ein Befehl zum Übersetzen, einer zur Validierung. Die Umschaltung funktioniert, weil jede JSON-Datei den Slug pro Sprache auflistet.
Das komplette Übersetzungssystem
Mein Übersetzungssystem besteht aus zwei Skripten, die zusammenarbeiten. Das Haupt-Orchestrator-Skript verarbeitet mehrere Sprachen, während das Worker-Skript die eigentliche KI-Übersetzung durchführt. Hier sind beide Skripte:
Haupt-Orchestrator-Skript (translate-all-languages.ts)
Dies ist das Skript, das Sie direkt ausführen. Es koordiniert die Übersetzung in alle 8 Sprachen und verwaltet Slug-Zuordnungen:
Klicken Sie zum Erweitern/Zuklappen des Orchestrator-Skripts
import fs from 'fs-extra';\nimport path from 'path';\nimport { execSync } from 'child_process';\n\ntype PostData = Record;\n\nconst SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de', 'ru', 'nl', 'it', 'zh'] as const;\nconst SOURCE_LOCALE = 'en';\n\ninterface TranslationConfig {\n locale: string;\n scriptCommand: string;\n}\n\nconst TRANSLATION_CONFIGS: TranslationConfig[] = [\n { locale: 'es', scriptCommand: 'npm run translate-es-gemini' },\n { locale: 'fr', scriptCommand: 'npm run translate-fr-gemini' },\n { locale: 'de', scriptCommand: 'npm run translate-de-gemini' },\n { locale: 'ru', scriptCommand: 'npm run translate-ru-gemini' },\n { locale: 'nl', scriptCommand: 'npm run translate-nl-gemini' },\n { locale: 'it', scriptCommand: 'npm run translate-it-gemini' },\n { locale: 'zh', scriptCommand: 'npm run translate-zh-gemini' },\n];\n\n/**\n * Generate slug mappings from translated file names\n */\nasync function generateSlugMappings(postSlug: string): Promise> {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', postSlug);\n const slugMappings: Record = {};\n\n if (!(await fs.pathExists(postsDir))) {\n throw new Error(\`Post directory not found: ${postsDir}\`);\n }\n\n // Start with English slug\n slugMappings[SOURCE_LOCALE] = postSlug;\n\n // Check for existing translation files to extract slugs\n for (const locale of SUPPORTED_LOCALES) {\n if (locale === SOURCE_LOCALE) continue;\n\n const translationFile = path.join(postsDir, \`index.${locale}.json\`);\n if (await fs.pathExists(translationFile)) {\n try {\n const translationData: PostData = await fs.readJson(translationFile);\n const translatedSlugs = translationData.translatedSlugs;\n if (translatedSlugs && translatedSlugs[locale]) {\n slugMappings[locale] = translatedSlugs[locale];\n } else {\n console.warn(\`Warning: Missing translated slug for ${locale} in ${postSlug}\`);\n slugMappings[locale] = postSlug; // Fallback to English slug\n }\n } catch (error) {\n console.warn(\`Warning: Could not read translation file for ${locale}: ${translationFile}\`);\n slugMappings[locale] = postSlug; // Fallback to English slug\n }\n } else {\n console.warn(\`Warning: Translation file missing for ${locale}: ${translationFile}\`);\n slugMappings[locale] = postSlug; // Fallback to English slug\n }\n }\n\n return slugMappings;\n}\n\n/**\n * Update all translation files with complete slug mappings\n */\nasync function updateSlugMappings(postSlug: string, slugMappings: Record) {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', postSlug);\n\n for (const locale of SUPPORTED_LOCALES) {\n const fileName = locale === SOURCE_LOCALE ? 'index.json' : \`index.${locale}.json\`;\n const filePath = path.join(postsDir, fileName);\n\n if (await fs.pathExists(filePath)) {\n try {\n const data: PostData = await fs.readJson(filePath);\n data.translatedSlugs = slugMappings;\n await fs.writeJson(filePath, data, { spaces: 2 });\n console.log(\`✅ Updated slug mappings in ${fileName}\`);\n } catch (error) {\n console.error(\`❌ Failed to update slug mappings in ${fileName}:\`, error);\n }\n }\n }\n}\n\n/**\n * Translate a single post to all languages\n */\nasync function translatePost(postSlug: string, force: boolean = false) {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', postSlug);\n const sourceFile = path.join(postsDir, 'index.json');\n\n // Verify source file exists\n if (!(await fs.pathExists(sourceFile))) {\n throw new Error(\`Source file not found: ${sourceFile}\`);\n }\n\n console.log(\`\\n🌍 Translating post: ${postSlug}\`);\n console.log(\`📁 Directory: ${postsDir}\`);\n\n // Translate to each target language\n for (const config of TRANSLATION_CONFIGS) {\n const targetFile = path.join(postsDir, \`index.${config.locale}.json\`);\n const exists = await fs.pathExists(targetFile);\n\n if (!exists || force) {\n try {\n console.log(\`\\n🔄 Translating to ${config.locale}...\`);\n const command = \`${config.scriptCommand} -- --only ${postSlug} ${force ? '--force' : ''}\`;\n execSync(command, { stdio: 'inherit', cwd: process.cwd() });\n console.log(\`✅ Successfully translated to ${config.locale}\`);\n \n // Small delay to be polite to the API\n await new Promise(resolve => setTimeout(resolve, 500));\n } catch (error) {\n console.error(\`❌ Failed to translate to ${config.locale}:\`, error);\n }\n } else {\n console.log(\`⏭️ Skipping ${config.locale} (already exists, use --force to overwrite)\`);\n }\n }\n\n // Generate and update slug mappings\n console.log(\`\\n🔗 Updating slug mappings...\`);\n try {\n const slugMappings = await generateSlugMappings(postSlug);\n await updateSlugMappings(postSlug, slugMappings);\n console.log(\`✅ Slug mappings updated for all languages\`);\n } catch (error) {\n console.error(\`❌ Failed to update slug mappings:\`, error);\n }\n}\n\n/**\n * Find all blog posts\n */\nasync function findAllPosts(): Promise {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts');\n \n if (!(await fs.pathExists(postsDir))) {\n throw new Error(\`Posts directory not found: ${postsDir}\`);\n }\n\n const entries = await fs.readdir(postsDir, { withFileTypes: true });\n return entries\n .filter(entry => entry.isDirectory())\n .map(entry => entry.name)\n .filter(name => !name.startsWith('.'));\n}\n\n/**\n * Validate translation completeness\n */\nasync function validateTranslations(postSlug?: string) {\n const posts = postSlug ? [postSlug] : await findAllPosts();\n let allValid = true;\n\n console.log(\`\\n🔍 Validating translations for ${posts.length} post(s)...\`);\n\n for (const slug of posts) {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', slug);\n console.log(\`\\n📋 Checking: ${slug}\`);\n\n for (const locale of SUPPORTED_LOCALES) {\n const fileName = locale === SOURCE_LOCALE ? 'index.json' : \`index.${locale}.json\`;\n const filePath = path.join(postsDir, fileName);\n \n if (await fs.pathExists(filePath)) {\n try {\n const data: PostData = await fs.readJson(filePath);\n \n // Check if translatedSlugs is complete\n const translatedSlugs = data.translatedSlugs || {};\n const missingLanguages = SUPPORTED_LOCALES.filter(lang => !translatedSlugs[lang]);\n \n if (missingLanguages.length > 0) {\n console.log(\` ⚠️ ${locale}: Missing slug mappings for ${missingLanguages.join(', ')}\`);\n allValid = false;\n } else {\n console.log(\` ✅ ${locale}: Complete\`);\n }\n } catch (error) {\n console.log(\` ❌ ${locale}: Invalid JSON\`);\n allValid = false;\n }\n } else {\n console.log(\` ❌ ${locale}: Missing file\`);\n allValid = false;\n }\n }\n }\n\n return allValid;\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const command = args[0];\n \n try {\n switch (command) {\n case 'translate': {\n const postSlug = args.find(arg => !arg.startsWith('--') && arg !== 'translate');\n const force = args.includes('--force');\n const all = args.includes('--all');\n\n if (all) {\n const allPosts = await findAllPosts();\n console.log(\`🚀 Translating all ${allPosts.length} posts...\`);\n \n for (const slug of allPosts) {\n await translatePost(slug, force);\n }\n } else if (postSlug) {\n await translatePost(postSlug, force);\n } else {\n console.error('❌ Please specify a post slug or use --all');\n process.exit(1);\n }\n break;\n }\n\n case 'validate': {\n const postSlug = args.find(arg => !arg.startsWith('--') && arg !== 'validate');\n const isValid = await validateTranslations(postSlug);\n \n if (isValid) {\n console.log('\\n✅ All translations are complete and valid!');\n } else {\n console.log('\\n⚠️ Some translations are incomplete or invalid.');\n process.exit(1);\n }\n break;\n }\n\n case 'update-slugs': {\n const postSlug = args.find(arg => !arg.startsWith('--') && arg !== 'update-slugs');\n \n if (postSlug) {\n const slugMappings = await generateSlugMappings(postSlug);\n await updateSlugMappings(postSlug, slugMappings);\n console.log(\`✅ Updated slug mappings for ${postSlug}\`);\n } else {\n console.error('❌ Please specify a post slug');\n process.exit(1);\n }\n break;\n }\n\n default:\n console.log(\`\n🌍 Blog Translation Manager\n\nUsage:\n npm run translate-all translate [--force] # Übersetze spezifischen Beitrag\n npm run translate-all translate --all [--force] # Übersetze alle Beiträge\n npm run translate-all validate [post-slug] # Überprüfe Übersetzungen\n npm run translate-all update-slugs # Aktualisiere Slug-Zuordnungen\n\nBeispiele:\n npm run translate-all translate how-to-read-nginx-access-logs\n npm run translate-all translate --all --force\n npm run translate-all validate\n npm run translate-all update-slugs how-to-read-nginx-access-logs\n \`);\n break;\n }\n } catch (error) {\n console.error('❌ Error:', error);\n process.exit(1);\n }\n}\n\nmain().catch(console.error);
Funktionsweise des Skripts
Das Skript macht mehrere Dinge:
- Übersetzungskoordination: Es ruft für jede Zielsprache einzelne Sprachübersetzungsskripte auf.
- Slug-Verwaltung: Es generiert und aktualisiert die
translatedSlugs
-Zuordnung in jeder Datei. - Validierung: Es prüft, ob alle Sprachdateien vorhanden sind und vollständige Slug-Zuordnungen aufweisen.
- Fehlerbehandlung: Es behandelt Fehler anmutig und fährt mit anderen Sprachen fort.
Die wichtigste Erkenntnis ist, dass jede Sprachdatei den Slug für jede andere Sprache kennen muss. Dies ermöglicht die korrekte Funktion der Sprachumschaltung.
Konfiguration der KI-API
So konfiguriere ich die Gemini-API-Aufrufe für eine zuverlässige Übersetzung:
Token-Limits und Parameter
- maxOutputTokens: 8192: Dies ist entscheidend für große Inhalte. Das Standardlimit ist viel niedriger, was zu abgeschnittenen Antworten und Parsing-Fehlern führte. Mit 8192 Token kann ich bis zu ~6.000 Wörter (Englisch) übersetzen, was die meisten Blogbeiträge und Artikel abdeckt.
- temperature: 0.2: Niedrige Temperatur sorgt für konsistente und vorhersehbare Übersetzungen über die Sprachen hinweg.
- topP: 0.95: Steuert die Vielfalt bei gleichzeitiger Aufrechterhaltung der Qualität.
- topK: 40: Begrenzt die Wortschatzwahl für fokussiertere Übersetzungen.
Token-Wort-Konvertierung
- Englische Eingabe: ~0,75 Wörter pro Token, also 8192 Token ≈ 6.000-6.500 Wörter
- Ausdehnung der Übersetzung: Die Ausgabe ist in der Regel 10-30 % länger als die Eingabe (Deutsch kann 20-30 % länger sein, Spanisch 10-20 % länger)
- HTML-Markup: Benutzt Token, wird aber nicht übersetzt, daher ist der tatsächlich übersetzbare Inhalt geringer.
- Beispiel aus der Praxis: Dieser Blogbeitrag (~2.400 Wörter) verwendet ~3.000 Eingabe-Token und ~4.000-5.000 Ausgabe-Token.
Strategie zur Inhaltsverarbeitung
- Zuerst kleine Felder: Titel, Auszug und Lesezeit in einem API-Aufruf übersetzen (schnell und zuverlässig).
- Übersetzung des vollständigen Inhalts: Für Inhalte über 2.000 Zeichen den gesamten Inhalt in einem Aufruf mit hohen Token-Limits übersetzen.
- Chunking-Fallback: Wenn die vollständige Übersetzung fehlschlägt, automatisch auf die Aufteilung des Inhalts in ~1.500 Zeichen lange Abschnitte zurückgreifen.
- Intelligentes JSON-Parsing: Antworten verarbeiten, die in Markdown-Codeblöcken (```json) enthalten sind, die die KI manchmal zurückgibt.
Fehlerbehandlung
- Modell-Fallback: Wenn das primäre Modell (gemini-2.5-flash-lite) nicht verfügbar ist, automatisch auf gemini-1.5-flash-latest wechseln.
- Anmutige Degradation: Wenn ein Abschnitt fehlschlägt, den ursprünglichen englischen Inhalt für diesen Abschnitt verwenden und fortfahren.
- API-Höflichkeit: Verzögerungen zwischen den Aufrufen hinzufügen, um die Ratenlimits einzuhalten.
Die wichtigste Lehre: Stellen Sie immer
maxOutputTokens
explizit ein. Die Standardlimits sind für reale Inhalte zu niedrig und verursachen mysteriöse Parsing-Fehler, die wie API-Probleme aussehen, aber tatsächlich Token-Limit-Probleme sind.
Tipps, wenn Sie dies versuchen
- Einfach schreiben. Kurze Sätze lassen sich gut übersetzen.
- Codeblöcke einzäunen und Inline-Code in
<code>
einbetten. - Wortwitz vermeiden, wenn Sie den Zieltext nicht überprüfen.
- Semantisches HTML verwenden. Überschriften und Listen helfen Lesern und Modellen.
- Halten Sie Ihre Quelldaten sauber und gut strukturiert.
FAQ
Bearbeite ich Übersetzungen?
Nur für Spanisch, da dies die einzige Sprache ist, die ich neben Englisch kenne. Für Titel und Intros möchte ich einen schnellen Ausgangspunkt haben und dann dort polieren, wo es wichtig ist. Für andere Sprachen verlasse ich mich auf die Qualität der KI-Übersetzung.
Datenschutz?
Inhalte werden an die Gemini-API gesendet. Ich sende keine Geheimnisse. API-Schlüssel befinden sich in
.env.local
(
GOOGLE_API_KEY
oder
GEMINI_API_KEY
).
Modelle?
Ich verwende ein schnelles Gemini-Modell und weiche automatisch aus, wenn es nicht verfügbar ist. Sie können mit
GEMINI_MODEL
überschreiben.
Was ist mit den alten URLs?
Ich lasse die alten URLs mit Weiterleitungen funktionieren. Das Slug-Mapping-System übernimmt dies automatisch.
Fazit
KI ersetzt Ihre Stimme nicht. Sie beseitigt die lästige Arbeit. Halten Sie Ihre Quelle sauber, automatisieren Sie die wiederholbaren Teile, validieren Sie und liefern Sie ab. Das von mir geteilte Skript übernimmt die Komplexität, damit Sie sich auf das Schreiben konzentrieren können.