Sieben Lokalisierungen, zwei Pipelines und warum ich mich überhaupt für Übersetzungen entschieden habe

Passwort vergessen

22. Mai 20269 min gelesen

Sieben Lokalisierungen, zwei Pipelines und warum ich mich überhaupt für Übersetzungen entschieden habe

Wie eine Person sieben Lokalisierungen ohne manuelle Bearbeitung jeder JSON-Datei pflegt und welche Einstellungsmärkte es wert waren, dafür zu bauen.

i18nportfoliofrontend

Ein Portfolio nur auf Englisch ist eine gültige Wahl. Ebenso ist es, wenn man als Einzelperson sieben Lokalisierungen pflegt: Es ist nicht "kostenlos", aber es ist lesbar, wenn man Sprache als Produktschwerpunkt behandelt, nicht als Checkbox, und die langweiligen Teile automatisiert.

Dieser Beitrag ist die lange Version: welche Sprachen, warum ich sie gewählt habe und genau was der Code tut (packages/types, packages/i18n, scripts/translate.mjs, apps/web/scripts/translate-blog.mjs).

Warum diese Sprachen (Strategie, nicht Laune)

Ich habe Lokalisierungen nicht gewählt, weil ein Template sie enthielt. Ich habe nicht für "meiste Sprecher weltweit" als einzelne Zahl optimiert. Ich begann bei der Einstellungsgeographie: Ich sah mir Länder und Regionen an, in denen Entwickler eingestellt werden (remote-freundliche Arbeitgeber, starke inländische Tech-Märkte, EU-Hubs) und fragte, welche Sprachen ich realistisch anbieten konnte, damit diese Leser die Seite in ihrer Muttersprache lesen konnten, nicht nur auf Englisch. Das ist eine Produktwahl darüber, wer diese Portfolio bewerten könnte, nicht Dekoration.

Englisch als Standard ist immer noch nicht verhandelbar: Es ist der breiteste gemeinsame Nenner für Tech. Die sechs nicht-englischen Lokalisierungen in diesem Repo sind Portugiesisch, Spanisch, Deutsch, Polnisch, Niederländisch und Estnisch, aufgelistet in LOCALE_CONFIG in packages/types/src/index.ts. Zusammen mit en sind das sieben Oberflächenlokalisierungen.

Die Absicht ist einfach: Wenn jemand, der mich einstellen oder zusammenarbeiten könnte, hier von Brasilien oder Portugal, Spanien oder Lateinamerika, Deutschland, Österreich oder der Schweiz, Polen, den Niederlanden oder Estland landet, möchte ich, dass er in einer Sprache liest, die zu dem passt, wie er tatsächlich liest, wenn es darauf ankommt, während ich immer noch akzeptiere, dass ein es viele Länder und ein pt zwei sehr unterschiedliche Märkte abdeckt. Das ist ein Kompromiss, keine Ignoranz. Ein Lokalisierungscode kann nicht jeden kulturellen Register dieser Sprachen abdecken, und ich tue nicht so, als ob er es könnte.

Klingen alle Sätze so, als ob ein menschlicher Übersetzer eine Stunde daran gearbeitet hätte? Nein. Maschinelle Übersetzung und LLM-unterstützte Batches sind Teil des Stapels. Das Versprechen ist enger und, denke ich, ehrlicher: bewusste Abdeckung, überprüfbare Diffs in git und Kopie, die nicht stillschweigend nur Englisch ist für Leser, die eine andere Muttersprache bevorzugen.

Was "sieben Lokalisierungen" in diesem Code bedeutet

Alles, was wissen muss, "welche Sprachen existieren", sollte von LOCALE_CONFIG abgeleitet werden: Schlüssel en, pt, es, de, pl, nl, et, jeweils mit name, bcp47 und einem franc-Code für Sprachdetektionshelfer. Das i18n-Paket exportiert diese Konfiguration erneut und erstellt locales als Object.keys(LOCALE_CONFIG); siehe packages/i18n/src/index.ts.

Laufzeitwörterbücher sind statische JSON-Dateien pro Lokalisierung unter packages/i18n/src/messages/, importiert durch eine kleine Importkarte, damit Bundler jede Datei auflösen können. Der htmlLangFromLocale-Helfer ordnet eine Lokalisierung einem BCP-47-Tag für <html lang> zu. Das ist die "einzige Wahrheitsquelle"-Geschichte: Typen + JSON-Dateien + Next-App stimmen alle auf dieselbe Aufzählung von Lokalisierungen überein.

Wenn Sie eine Lokalisierung hinzufügen, erweitern Sie LOCALE_CONFIG, fügen eine neue JSON-Datei hinzu und verknüpfen den Importer, dann führen Sie die Übersetzungs-Pipeline aus. Es gibt keine versteckte Laufzeitliste, die woanders abweichen kann.

// packages/types/src/index.ts
export const LOCALE_CONFIG = {
  en: { name: "English",    bcp47: "en", franc: "eng" },
  pt: { name: "Portugês",  bcp47: "pt", franc: "por" },
  es: { name: "Español",    bcp47: "es", franc: "spa" },
  de: { name: "Deutsch",    bcp47: "de", franc: "deu" },
  pl: { name: "Polski",     bcp47: "pl", franc: "pol" },
  nl: { name: "Nederlands", bcp47: "nl", franc: "nld" },
  et: { name: "Eesti",      bcp47: "et", franc: "est" },
} as const;

export type Locale = keyof typeof LOCALE_CONFIG;

Pipeline A: UI-Strings, en.json, Lockfile, hybride MT/LLM

Die maßgebliche UI-Kopie lebt in packages/i18n/src/messages/en.json. Andere Sprachen werden generiert; die Projektregel ist explizit: bearbeiten Sie nur Englisch, dann führen Sie pnpm translate vom Repository-Root aus, was scripts/translate.mjs mit Umgebung aus apps/web/.env.local ausführt (siehe das Root-package.json-Skript).

Inkrementelle Übersetzung und die Lockfile

translate.mjs ist lang aus guten Gründen. Es verwaltet scripts/translate.lock.json: pro Lokalisierung Hashes von Abschnitten der englischen Quelle, damit Sie, wenn Sie einen Teil von en.json ändern, nicht blind die gesamte Wörterbuch an eine API senden. Das ist wichtig, wenn en.json wächst (über Seiten, Projekte, Erfahrung) und Sie sich um Kosten und Wiederholbarkeit kümmern.

Das Skript entfernt auch Schlüssel, die aus dem Englischen verschwunden sind, damit Lokalisierungsdateien nicht angesammelte tote Zweige akkumulieren, und es kann bekannte unübersetzbare Begriffe (Eigennamen, Tech-Marken) vor einer Anfrage entfernen und dann wieder einfügen; siehe die DO_NOT_TRANSLATE-Menge und verwandte Helfer in der Datei.

Hybride Routing: LibreTranslate vs. Groq

Die Wahl der Werkzeuge wurde von Kosten und Eignung getrieben. LibreTranslate ist selbst-hostbar via Docker ohne Token-Kosten, was es zum richtigen Fit für kurze, informative Strings wie nav, meta und errors macht; diese Schlüssel benötigen keine LLM-Nuancen, und die Bezahlung pro Zeichen an DeepL oder Google Translate würde sich ohne Qualitätsgewinn summieren. Groq übernimmt den LLM-Pfad, weil seine Inferenz schnell und billig für Batch-Arbeiten im Vergleich zu OpenAI oder Anthropic bei demselben Volumen ist; das Standard-TRANSLATE_MODEL ist ein kleines Modell (llama-4-scout), das speziell ausgewählt wurde, um die Kosten pro Lauf niedrig zu halten, nicht dasselbe Modell, das für den Chat verwendet wird.

Das Skript sendet nicht alles an Groq. Ein CONFIG-Objekt kodiert die reale Richtlinie:

  • Erzählschlüssel, die Mustern in groqKeyPatterns entsprechen (z. B. about.sections.*.body, home.hero.subheadline, projects.items.*.problem, experience.items.*.impact) gehen durch Groq für jede Lokalisierung via shouldUseGroqForKey. Berufsbezeichnungen (experience.items.*.role) sind auch immer Groq, weil reine MT oft englisch aussehende Titel unverändert zurückgibt.
  • Kürzere oder informativere Namensräume werden zu LibreTranslate via libreKeyPrefixes (z. B. meta, nav, errors, experience.labels) tendiert, mit LIBRETRANSLATE_URL, das standardmäßig auf einen lokalen Docker-freundlichen Host gesetzt wird, es sei denn, Sie überschreiben es.

Also: jede Lokalisierung erhält LLM-Qualität auf Erzählfeldern; LibreTranslate behandelt die informativen Schlüssel, bei denen maschinelle Übersetzung genau genug ist. Das ist ein Kosten- und Qualitätstrade-off, der in Code eingebaut ist, nicht ein Kommentar in einer README.

// scripts/translate.mjs
function shouldUseGroqForKey(localeCode, key) {
  return (
    matchesDotPattern(key, "experience.items.*.role") ||
    CONFIG.groqKeyPatterns.some((pat) => matchesDotPattern(key, pat))
  );
}

Umgebung und Operationen, die Sie tatsächlich berühren

Die beiden Groq-Schlüssel sind absichtlich getrennt. GROQ_API_KEY lebt in der Produktion und bedient den Portfolio-Chat zur Laufzeit; Übersetzungsbatch-Läufe, die Token-Verbrauch erhöhen würden, würden in dieselbe Rate-Limit einwirken und echte Benutzer beeinträchtigen. TRANSLATE_GROQ_API_KEY ist ein anderer Schlüssel, der nur in lokalen Batch-Läufen verwendet wird, mit seiner eigenen Quote. Der Fallback auf GROQ_API_KEY existiert für Bequemlichkeit, wenn kein dedizierter Schlüssel konfiguriert ist, nicht als beabsichtigte Einrichtung.

TRANSLATE_MODEL wählt das Modell für die Batch-Übersetzung (Standard meta-llama/llama-4-scout-17b-16e-instruct im Skript-Header), unabhängig von GROQ_MODEL, das vom Chat verwendet wird. Der Standard ist absichtlich ein kleines, schnelles Modell, um die Kosten pro Lauf niedrig zu halten. Es gibt Knöpfe für Batch-Größe, 429-Wiederholungen und Protokollierung; lesen Sie den Kommentarblock am Anfang der Datei in scripts/translate.mjs, bevor Sie ihn anpassen.

Die API-Nutzung für Groq (und Hosting von LibreTranslate, wenn Sie es lokal ausführen) ist echtes Geld. Der Gewinn hier ist Kontrolle und Automatisierung, nicht Magie. Die Lockfile ist, was es handhabbar macht: Abschnitte, die sich im Englischen nicht geändert haben, gehen nicht an eine API beim nächsten Lauf.

Pipeline B: Blog-Beiträge, englisches MDX, dann Groq-only-Batch

Blog-Beiträge werden nicht mit demselben hybriden Router übersetzt. pnpm translate:blog führt apps/web/scripts/translate-blog.mjs aus, das apps/web/content/blog/en/*.mdx liest und für jede Ziel-Lokalisierung in TARGET_LOCALES (pt, es, de, pl, nl, et) Groq mit einer strengen Aufforderung aufruft: bewahren Sie die YAML-Frontmatter-Struktur, behalten Sie date und hero identisch mit der englischen Quelle, übersetzen Sie Titel, Beschreibung, Tags und Körper, verzerren Sie keine URLs. Ausgabedateien landen unter apps/web/content/blog/<locale>/.

Es gibt keinen LibreTranslate-Pfad in diesem Skript heute; die Blog-Übersetzung ist LLM-Batch pro Datei, mit --locale, --file, --force und --delay, um Rate-Limit-Schmerzen zu reduzieren. Zur Lesezeit lädt Next einfach das MDX für die Route-Lokalisierung; kein Groq-Aufruf passiert, wenn ein Besucher eine Blog-Seite öffnet.

Laufzeit: Routen, Wörterbücher und Blog-Pfade

Die Next.js-App verwendet das [locale]-Segment unter apps/web/app/[locale]/. Server-Komponenten rufen normalizeLocale für den Parameter auf, damit unbekannte Werte auf en zurückfallen, dann lädt getDictionary(locale) das passende JSON-Modul aus packages/i18n. Deshalb sieht jede Seitenvorlage strukturell gleich aus: Sie lesen alle Kopie aus dem Wörterbuch, keine hartkodierten englischen Strings in JSX (mit seltenen Ausnahmen, die der Linter flaggt).

Blog-Routen kombinieren diese Lokalisierung mit dem Slug: veröffentlichte Beiträge werden aus content/blog/<locale>/<slug>.mdx gelesen, wenn der Slug in publish.json aktiviert ist. Also ist das "Unterstützen von sieben Lokalisierungen" nicht nur Übersetzungsskripte; es sind auch statische Routen + statische Inhaltsdateien, die für jede Sprache existieren müssen, die Sie betreuen.

Das Versprechen auf Qualität ist enger als literarischer Glanz: konsistente Pipelines, überprüfbare Ausgaben in git und ehrliche Priorisierung: pt/es-Erzählfelder via Groq, alles andere via MT, es sei denn, der Schlüssel entspricht dem Rollenmuster. Das ist kein Versagensmodus; es ist ein erklärtes Ziel.

Wenn Sie dasselbe aufbauen

Beginnen Sie bei denjenigen, die Sie lesen möchten und welche Einstellungsmärkte Ihnen wichtig sind, nicht von "wie vielen Flaggen". Die Liste der Lokalisierungen ist eine Produktentscheidung; behandeln Sie sie wie eine.

Beginnen Sie klein. Eine zusätzliche Lokalisierung (wahrscheinlich diejenige, in der Sie berufliche Verbindungen haben oder aktiv auf Jobsuche sind) reicht aus, um die Pipeline zu validieren. Fügen Sie einen LOCALE_CONFIG-Eintrag hinzu, fügen Sie die JSON-Datei hinzu, führen Sie pnpm translate aus. Wenn die Ausgabequalität für diese Lokalisierung akzeptabel ist, haben Sie einen wiederholbaren Pfad. Sie benötigen nicht sechs nicht-englische Lokalisierungen, um zu beweisen, dass die Architektur funktioniert.

Entscheiden Sie sich früh für Ihre hybride Richtlinie. LibreTranslate ist schnell und kostenlos, wenn Sie es lokal ausführen; Groq fügt Qualität zu Erzählfeldern hinzu, aber kostet Token und fügt Latenz hinzu. Wenn Sie nur eine oder zwei nicht-englische Lokalisierungen anbieten, ist Groq-only für das gesamte Wörterbuch einfacher als die Routing-Logik in shouldUseGroqForKey aufzubauen. Die hybride Komplexität in translate.mjs lohnt sich nur, wenn Sie genug Schlüssel und genug Lokalisierungen haben, dass die Qualitätsschwankung von MT tatsächlich über sie hinweg wichtig ist.

Behalten Sie die englische Quelle kanonisch und die Ausgabe diff-überprüfbar. Die Lockfile in scripts/translate.lock.json ist kein optionaler Schmuck; sie ist, was Ihnen ermöglicht, pnpm translate auszuführen, ohne Token für Abschnitte auszugeben, die sich nicht geändert haben. Jede Pipeline, die das gesamte Wörterbuch bei jedem Lauf neu übersetzt, wird Kosten- und Rate-Limit-Probleme erleben, bevor Ihr Portfolio einen zweiten Leser hat. Committen Sie die generierten Lokalisierungsdateien, damit Sie jede Übersetzungsänderung in einem normalen Pull-Request überprüfen können.

Dann implementieren Sie eine englische Quelle, generierte Satellitendateien und Skripte, die Sie in CI oder lokal ausführen können, damit Ihr Portfolio ein Produkt bleibt, das Sie weiterentwickeln können, kein Stapel handeditierter Kopien.

Zusammenfassung

Die Implementierung ist nicht der schwierigste Teil. LOCALE_CONFIG, zwei Batch-Skripte, eine Lockfile: Jeder anständige Ingenieur kann das an einem Wochenende aufbauen. Der schwierigere Teil ist, sich im Voraus zu entscheiden, dass Sprache eine Produktbeschränkung ist, kein Feature, das Sie hinzufügen, wenn der Rest "fertig" ist.

Wenn Sie i18n als Dekoration behandeln, werden Sie es genau einmal ausliefern und nie wieder anfassen. Wenn Sie es als Umfang behandeln, mit Automatisierung, überprüfbaren Diffs und einer ehrlichen Liste dessen, was es nicht abdeckt, und es handhabbar bleibt, und Sie die Kontrolle darüber behalten, was Ihr Portfolio Lesern kommuniziert, die Sie vielleicht nie in Englisch treffen.

Das ist die eigentliche Wette hier: nicht, dass jeder Satz so klingt, als ob ein menschlicher Übersetzer eine Stunde daran gearbeitet hätte, sondern dass die Seite funktioniert für jemanden in Warschau oder Tallinn, der es vorzieht, nicht auf Englisch zu lesen, wenn es darauf ankommt. Ob sich diese Wette auszahlt, ist noch unklar.