Lantern integrieren (Ihre Übersetzungsdateien synchron halten)

Auf dieser Seite

Lantern ist die Quelle der Wahrheit für Ihre Übersetzungen. Ihre Anwendung hält lokale Locale-Dateien (locales/en.json, locales/fr.json, …) synchron, indem sie sie über die Lese-API abruft — typischerweise als Schritt in Ihrem Build oder Deployment.

  • Basis-URL: https://lantern.abyss-inn.ch
  • Authentifizierung: ein API-Token aus dem Tab API des Projekts (pro Projekt). Token sind standardmäßig schreibgeschützt; mit Schreibberechtigungen kann ein KI-Agent oder Ihre CI Schlüssel und Übersetzungen zurückschreiben (siehe Zurückschreiben)
  • Endpunkt: GET /api/v1/projects/<slug>/translations

Der Endpunkt#

GET /api/v1/projects/<slug>/translations
Authorization: Bearer lk_…
Query-Parameter Werte Bedeutung
format json (Standard), csv Ausgabeformat
locale ein Locale-Code (z. B. en) Eine Sprache. Weglassen für alle Sprachen
nested 1 Verschachteltes JSON ({"home":{"title":…}}) statt flach ({"home.title":…})

Antworten

  • ?locale=en&nested=1 → eine Sprache:
    { "home": { "title": "Welcome" } }
    
  • (ohne locale) ?nested=1 → alle Sprachen, nach Locale geschlüsselt (am besten zum Synchronisieren von Dateien):
    { "en": { "home": { "title": "Welcome" } }, "fr": { "home": { "title": "Bienvenue" } } }
    
  • format=csv → eine key-Spalte plus eine Spalte je Locale.

Fehler: 401 (Token fehlt/ungültig), 403 (Token passt nicht zum Slug), 400 (unbekannte Locale / ungültiges Format), 404 (Projekt nicht gefunden).

Um die Sprachen des Projekts mit Anzeigenamen aufzulisten (der Aufruf oben liefert nur Locale-Codes), verwenden Sie:

GET /api/v1/projects/<slug>/languages
Authorization: Bearer lk_…
→ 200 [ { "locale": "en", "name": "English", "isDefault": true }, … ]

Token-Sicherheit#

  • Das Token ist schreibgeschützt und auf ein Projekt beschränkt. Legen Sie es als Umgebungsvariable / CI-Secret ab (z. B. LANTERN_TOKEN) — committen Sie es niemals.
  • Zum Rotieren: im API-Tab widerrufen und ein neues erstellen.

Synchronisierungsstrategien#

  1. Abruf zur Build-Zeit (empfohlen). Führen Sie einen Pull-Schritt vor dem Build aus, damit die ausgelieferten Dateien immer aktuell sind. Ob Sie das erzeugte locales/ per .gitignore ausschließen oder committen, bleibt Ihnen überlassen.
  2. Abruf zur Laufzeit + Cache. Langlaufende Server können beim Start abrufen und in Intervallen aktualisieren. Cachen Sie im Speicher und greifen Sie bei einem Fehler auf die letzte gute Kopie zurück.
  3. Geplant. Für Laufzeit-Anwendungen ohne Redeploy: den Pull per Cron ausführen und neu laden.

Code#

Jedes Beispiel unten holt alle Locales in einer Anfrage (?nested=1) und schreibt locales/<locale>.json. Um stattdessen nur eine Sprache zu holen, hängen Sie ?locale=en an und schreiben eine einzelne Datei. Setzen Sie LANTERN_TOKEN in Ihrer Umgebung und ersetzen Sie my-project durch Ihren Slug.

Shell (curl + jq) — universell#

#!/usr/bin/env bash
set -euo pipefail
: "${LANTERN_TOKEN:?set LANTERN_TOKEN}"
BASE=https://lantern.abyss-inn.ch ; SLUG=my-project ; OUT=locales
mkdir -p "$OUT"
curl -fsS -H "Authorization: Bearer $LANTERN_TOKEN" \
  "$BASE/api/v1/projects/$SLUG/translations?nested=1" \
| jq -c 'to_entries[]' | while read -r e; do
    loc=$(jq -r '.key' <<<"$e")
    jq '.value' <<<"$e" > "$OUT/$loc.json"
    echo "wrote $OUT/$loc.json"
  done

JavaScript (Node)#

Eine fertige CLI wird mitgeliefert — für JS/TS-Projekte ist das der einfachste Weg:

LANTERN_TOKEN=lk_… node cli/lantern-pull.mjs \
  --url https://lantern.abyss-inn.ch --project my-project --out locales

Entsprechung als Inline-Code:

import { mkdir, writeFile } from "node:fs/promises";

const BASE = "https://lantern.abyss-inn.ch", SLUG = "my-project";
const res = await fetch(`${BASE}/api/v1/projects/${SLUG}/translations?nested=1`, {
  headers: { Authorization: `Bearer ${process.env.LANTERN_TOKEN}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const all = await res.json();

await mkdir("locales", { recursive: true });
for (const [loc, messages] of Object.entries(all)) {
  await writeFile(`locales/${loc}.json`, JSON.stringify(messages, null, 2) + "\n");
}

TypeScript#

import { mkdir, writeFile } from "node:fs/promises";

type Messages = Record<string, unknown>;
type AllLocales = Record<string, Messages>;

const BASE = "https://lantern.abyss-inn.ch", SLUG = "my-project";
const res = await fetch(`${BASE}/api/v1/projects/${SLUG}/translations?nested=1`, {
  headers: { Authorization: `Bearer ${process.env.LANTERN_TOKEN}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const all = (await res.json()) as AllLocales;

await mkdir("locales", { recursive: true });
for (const [loc, messages] of Object.entries(all)) {
  await writeFile(`locales/${loc}.json`, JSON.stringify(messages, null, 2) + "\n");
}

Python (Standardbibliothek, ohne Abhängigkeiten)#

import json, os, pathlib, urllib.request

BASE, SLUG = "https://lantern.abyss-inn.ch", "my-project"
req = urllib.request.Request(
    f"{BASE}/api/v1/projects/{SLUG}/translations?nested=1",
    headers={"Authorization": f"Bearer {os.environ['LANTERN_TOKEN']}"},
)
all_locales = json.load(urllib.request.urlopen(req))

out = pathlib.Path("locales"); out.mkdir(exist_ok=True)
for loc, messages in all_locales.items():
    (out / f"{loc}.json").write_text(
        json.dumps(messages, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
    )
    print("wrote", loc)

C##

using System.Net.Http.Headers;
using System.Text.Json;

var baseUrl = "https://lantern.abyss-inn.ch";
var slug = "my-project";
var token = Environment.GetEnvironmentVariable("LANTERN_TOKEN")!;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

var json = await http.GetStringAsync(
    $"{baseUrl}/api/v1/projects/{slug}/translations?nested=1");

using var doc = JsonDocument.Parse(json);
Directory.CreateDirectory("locales");
var opts = new JsonSerializerOptions { WriteIndented = true };
foreach (var locale in doc.RootElement.EnumerateObject())
{
    var text = JsonSerializer.Serialize(locale.Value, opts);
    await File.WriteAllTextAsync($"locales/{locale.Name}.json", text);
    Console.WriteLine($"wrote {locale.Name}");
}

Rust#

# Cargo.toml
[dependencies]
ureq = { version = "2", features = ["json"] }
serde_json = "1"
use std::{env, fs};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let base = "https://lantern.abyss-inn.ch";
    let slug = "my-project";
    let token = env::var("LANTERN_TOKEN")?;
    let url = format!("{base}/api/v1/projects/{slug}/translations?nested=1");

    let all: serde_json::Value = ureq::get(&url)
        .set("Authorization", &format!("Bearer {token}"))
        .call()?
        .into_json()?;

    fs::create_dir_all("locales")?;
    for (loc, messages) in all.as_object().unwrap() {
        fs::write(
            format!("locales/{loc}.json"),
            serde_json::to_string_pretty(messages)?,
        )?;
        println!("wrote {loc}");
    }
    Ok(())
}

Go#

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	base, slug := "https://lantern.abyss-inn.ch", "my-project"
	req, _ := http.NewRequest("GET", base+"/api/v1/projects/"+slug+"/translations?nested=1", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("LANTERN_TOKEN"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var all map[string]json.RawMessage
	if err := json.NewDecoder(res.Body).Decode(&all); err != nil {
		panic(err)
	}

	os.MkdirAll("locales", 0o755)
	for loc, msgs := range all {
		var pretty any
		json.Unmarshal(msgs, &pretty)
		out, _ := json.MarshalIndent(pretty, "", "  ")
		os.WriteFile("locales/"+loc+".json", out, 0o644)
		fmt.Println("wrote", loc)
	}
}

PHP#

<?php
$base = "https://lantern.abyss-inn.ch";
$slug = "my-project";
$token = getenv("LANTERN_TOKEN");

$ctx = stream_context_create(["http" => ["header" => "Authorization: Bearer $token"]]);
$json = file_get_contents("$base/api/v1/projects/$slug/translations?nested=1", false, $ctx);
$all = json_decode($json, true);

@mkdir("locales");
foreach ($all as $loc => $messages) {
    file_put_contents(
        "locales/$loc.json",
        json_encode($messages, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
    );
    echo "wrote $loc\n";
}

Zurückschreiben — Schlüssel und Übersetzungen senden#

Die Lese-Synchronisierung ist eine Einbahnstraße (Lantern → Ihre Dateien). Ein Token mit Schreibberechtigungen kann auch in die andere Richtung senden — ideal für einen KI-Agenten, der Übersetzungen entwirft, oder einen CI-Schritt, der im Quellcode gefundene neue Schlüssel registriert. Vergeben Sie die Scopes beim Erstellen des Tokens im Tab API:

Scope Erlaubt
key:create Übersetzungsschlüssel erstellen
key:edit Einen Schlüssel umbenennen / Namensraum oder Beschreibung ändern
translation:edit Übersetzungswerte setzen/überschreiben
language:manage Eine fehlende Zielsprache beim Senden automatisch anlegen

Endpunkte#

POST /api/v1/projects/<slug>/keys
Authorization: Bearer lk_…            (needs key:create)
{ "key": "home.title", "description": "Landing headline" }
→ 201 { "id": "…", "key": "home.title", "created": true }   (409 if it exists)
PATCH /api/v1/projects/<slug>/keys
Authorization: Bearer lk_…            (needs key:edit)
{ "key": "home.title", "namespace": "home", "name": "headline" }
→ 200 { "id": "…", "key": "home.headline", "updated": true }   (409 if it collides)

Identifizieren Sie den Schlüssel über seinen aktuellen punktierten key-Pfad oder über keyId; die übrigen Felder (name, namespace, description) sind die neuen Werte, und alles, was Sie weglassen, behält seinen aktuellen Wert. Aktualisiert wird über die id, daher überstehen Übersetzungen, Status und Kommentare des Schlüssels eine Umbenennung — kein Löschen und Neuanlegen.

PUT /api/v1/projects/<slug>/translations
Authorization: Bearer lk_…            (needs translation:edit)
{ "locale": "fr", "createMissingKeys": true,
  "entries": [ { "key": "home.title", "value": "Bienvenue" } ] }
→ 200 { "updated": 1, "created": 0, "skipped": 0 }

createMissingKeys erfordert zusätzlich key:create; das Senden an eine noch nicht existierende Locale erfordert language:manage (sonst wird die Anfrage mit 400 Unknown locale abgelehnt). Token-Schreibvorgänge erscheinen im Aktivitäts-Feed des Projekts, zugeordnet zum Namen des Tokens.

Push-CLI#

Mitgeliefert wird cli/lantern-push.mjs (ohne Abhängigkeiten, das Gegenstück zur Pull-CLI):

# Register keys (a JSON array of dotted paths, or [{ "key", "namespace", "description" }])
LANTERN_TOKEN=lk_… node cli/lantern-push.mjs \
  --url https://lantern.abyss-inn.ch --project my-project --keys keys.json

# Push one locale from a flat or nested JSON file (creating any missing keys)
LANTERN_TOKEN=lk_… node cli/lantern-push.mjs \
  --url https://lantern.abyss-inn.ch --project my-project \
  --locale fr --in fr.json --create-keys

Fehler: 401 (ungültiges Token), 403 (fehlender Scope oder Token passt nicht zum Slug), 400 (ungültiger Body / unbekannte Locale), 409 (Schlüssel existiert bereits).


Lantern aus einem KI-Agenten nutzen (MCP)#

Lantern betreibt einen MCP-Server, damit ein KI-Agent (Claude Code / Claude Desktop usw.) die Übersetzungen eines Projekts im Dialog lesen und schreiben kann — kein eigener Klebecode, nichts zu installieren. Die Scopes des Tokens entscheiden, was der Agent darf (ein schreibgeschütztes Token kann lesen, die Schreibwerkzeuge antworten mit 403).

Erstellen Sie im Tab API des Projekts ein lk_-Token und richten Sie Ihren MCP-Client auf den Endpunkt:

{
  "mcpServers": {
    "lantern": {
      "url": "https://lantern.abyss-inn.ch/api/v1/mcp",
      "headers": { "Authorization": "Bearer lk_…" }
    }
  }
}

Das Token identifiziert bereits das Projekt — mehr ist nicht zu konfigurieren.

Werkzeuge, die dem Agenten zur Verfügung stehen:

Werkzeug Zweck Benötigter Scope
list_projects Die Projekte, auf die dieses Token zugreifen kann
list_languages Die Sprachen eines Projekts (Locale + Anzeigename + Standard)
get_translations Übersetzungen lesen (locale?, nested?, format?)
create_key Einen Schlüssel erstellen key:create
set_translations Werte einer Locale anlegen/aktualisieren (createMissingKeys?) translation:edit

createMissingKeys erfordert zusätzlich key:create; das automatische Anlegen einer neuen Locale erfordert language:manage.

Ein lk_-Token ist auf ein Projekt beschränkt, die Werkzeuge arbeiten also automatisch darauf. Eine benutzerweite OAuth-Verbindung (siehe unten) erreicht alle Ihre Projekte, deshalb nehmen ihre Werkzeuge einen project-Slug entgegen — rufen Sie zuerst list_projects auf und übergeben Sie dann project an die übrigen. Die Berechtigungen werden bei jedem Aufruf pro Projekt erneut geprüft, der Agent kann in einem Projekt also nie mehr tun als Sie selbst.

Mit OAuth verbinden (anmelden statt ein Token einfügen)#

Wenn Ihr MCP-Client OAuth unterstützt (Claudes Connectors, viele IDE-Agenten), können Sie sich durch Anmelden verbinden, statt ein lk_-Token zu erstellen und einzufügen. Richten Sie den Client ohne Authorization-Header auf dieselbe URL:

{
  "mcpServers": {
    "lantern": { "url": "https://lantern.abyss-inn.ch/api/v1/mcp" }
  }
}

Der Client findet Lanterns Autorisierungsserver automatisch, öffnet einen Browser zur Anmeldung und zeigt einen Zustimmungsdialog, der bestätigt, dass der Agent als Sie, über alle Ihre Projekte hinweg handelt. Eine Autorisierung deckt jedes Projekt ab, das Sie sehen können (der Agent wählt pro Aufruf eines über das Argument project); Sie können nur Berechtigungen vergeben, die Sie selbst besitzen, und jede wird zur Aufrufzeit pro Projekt erneut geprüft. Der Zugriff hängt an Ihrem Konto — widerrufen Sie ihn jederzeit, indem Sie sich anderswo anmelden oder die Eigentümerin bzw. den Eigentümer des Workspace kontaktieren. Lantern implementiert Standard-OAuth 2.1 (PKCE, dynamische Client-Registrierung, rotierende Refresh-Token), sodass jeder spezifikationskonforme MCP-Client funktioniert.

Die lk_-Token-Methode oben funktioniert unverändert weiter — sie ist die richtige Wahl für CLIs, Skripte und CI, wo es keinen Browser für eine Anmeldung gibt.

In der CI (GitHub Actions)#

Fügen Sie LANTERN_TOKEN als Repository-Secret hinzu und holen Sie die Dateien vor dem Build:

- name: Pull translations from Lantern
  env:
    LANTERN_TOKEN: ${{ secrets.LANTERN_TOKEN }}
  run: |
    node cli/lantern-pull.mjs \
      --url https://lantern.abyss-inn.ch --project my-project --out locales

(Oder rufen Sie das Pull-Skript Ihrer Sprache von oben auf.) Der Build bündelt dann die frischen Dateien.

Die Dateien an eine i18n-Bibliothek anschließen#

Das verschachtelte JSON entspricht dem, was die meisten i18n-Bibliotheken erwarten, z. B.:

  • JS/TS: i18next (resources), react-intl, vue-i18n, next-intl
  • Python: das JSON laden und über den punktierten Schlüssel nachschlagen (oder flache Ausgabe: nested=1 weglassen)
  • C#: an JSON-Quellen für IStringLocalizer binden oder direkt lesen
  • Rust: fluent/rust-i18n (hier ist die flache Ausgabe oft angenehmer — nested=1 weglassen)

Wenn Ihre Bibliothek flache Schlüssel erwartet ("home.title"), lassen Sie nested=1 in der URL weg.

Unity (Unity Localization)#

Verdrahten Sie für Unity nicht das rohe JSON — nutzen Sie Unitys offizielles Localization-Paket als Runtime (Locales, String Tables, LocalizedString, TMP-Unterstützung) und lassen Sie Lantern es füttern.

Das Paket lantern-unity ist eine reine Editor-Brücke: Ein Klick auf Window ▸ Lantern ▸ Pull Translations holt ein Projekt (über die format=csv-Ausgabe der Lese-API) direkt in eine String Table Collection — ohne manuelle Dateien. Installieren Sie es im Package Manager über Add package from git URL…:

https://github.com/Rabzizz/lantern-unity.git#v0.1.0

com.unity.localization ist eine Abhängigkeit und wird automatisch mitinstalliert. Verwenden Sie ein schreibgeschütztes lk_-Token; es wird pro Rechner in EditorPrefs gespeichert und landet nie in Ihrem Build. Vorerst ist die Brücke schreibgeschützt (Lantern → Unity).

Kein Paket, nur CSV? Unity Localization kann Lanterns CSV direkt importieren: Speichern Sie …/translations?format=csv in einer Datei, fügen Sie Ihrer String Table Collection die CSV-Erweiterung hinzu, verweisen Sie auf die Datei und klicken Sie Import. Das Paket automatisiert genau diesen Weg.