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→ einekey-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#
- 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.gitignoreausschließen oder committen, bleibt Ihnen überlassen. - 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.
- 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=1weglassen) - C#: an JSON-Quellen für
IStringLocalizerbinden oder direkt lesen - Rust:
fluent/rust-i18n(hier ist die flache Ausgabe oft angenehmer —nested=1weglassen)
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.