Lantern è la fonte di verità per le tue traduzioni. La tua applicazione mantiene file di
lingua locali (locales/en.json, locales/fr.json, …) sincronizzati scaricandoli
dall'API di lettura — di solito come passaggio della build o del deploy.
- URL di base:
https://lantern.abyss-inn.ch - Autenticazione: un token API dalla scheda API del progetto (uno per progetto). I token sono in sola lettura per impostazione predefinita; concedere permessi di scrittura consente a un agente IA o alla CI di rimandare indietro chiavi e traduzioni (vedi Scrittura inversa)
- Endpoint:
GET /api/v1/projects/<slug>/translations
L'endpoint#
GET /api/v1/projects/<slug>/translations
Authorization: Bearer lk_…
| Parametro | Valori | Significato |
|---|---|---|
format |
json (predefinito), csv |
Formato di output |
locale |
un codice lingua (es. en) |
Una sola lingua. Omettilo per tutte |
nested |
1 |
JSON annidato ({"home":{"title":…}}) invece che piatto ({"home.title":…}) |
Risposte
?locale=en&nested=1→ una sola lingua:{ "home": { "title": "Welcome" } }- (senza
locale)?nested=1→ tutte le lingue, indicizzate per lingua (ideale per sincronizzare i file):{ "en": { "home": { "title": "Welcome" } }, "fr": { "home": { "title": "Bienvenue" } } } format=csv→ una colonnakeypiù una colonna per lingua.
Errori: 401 (token mancante/non valido), 403 (il token non corrisponde allo slug),
400 (lingua sconosciuta / formato non valido), 404 (progetto non trovato).
Per elencare le lingue del progetto con i nomi visualizzati (la chiamata sopra espone solo i codici lingua), usa:
GET /api/v1/projects/<slug>/languages
Authorization: Bearer lk_…
→ 200 [ { "locale": "en", "name": "English", "isDefault": true }, … ]
Sicurezza dei token#
- Il token è in sola lettura ed è limitato a un solo progetto. Conservalo come variabile
d'ambiente / segreto di CI (per esempio
LANTERN_TOKEN) — non committarlo mai. - Per ruotarlo: revocalo nella scheda API e creane uno nuovo.
Strategie di sincronizzazione#
- Download in fase di build (consigliato). Esegui un passaggio di pull prima della build
così che i file distribuiti siano sempre aggiornati. Puoi mettere la cartella
locales/generata in.gitignoreoppure committarla — decidi tu. - Download a runtime + cache. I server a lunga esecuzione possono scaricare all'avvio e aggiornare a intervalli. Tieni una cache in memoria e in caso di errore ripiega sull'ultima copia valida.
- Pianificata. Per applicazioni a runtime che non vengono ridistribuite, pianifica il pull con cron e ricarica.
Codice#
Ogni esempio qui sotto scarica tutte le lingue in un'unica richiesta (?nested=1) e
scrive locales/<locale>.json. Per scaricare invece una sola lingua, aggiungi ?locale=en e
scrivi un solo file. Imposta LANTERN_TOKEN nel tuo ambiente e sostituisci my-project con
il tuo slug.
Shell (curl + jq) — universale#
#!/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)#
È inclusa una CLI pronta all'uso — per i progetti JS/TS è la via più semplice:
LANTERN_TOKEN=lk_… node cli/lantern-pull.mjs \
--url https://lantern.abyss-inn.ch --project my-project --out locales
Equivalente inline:
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 (libreria standard, senza dipendenze)#
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";
}
Scrittura inversa — inviare chiavi e traduzioni#
La sincronizzazione in lettura è a senso unico (Lantern → i tuoi file). Un token con permessi di scrittura può inviare anche nella direzione opposta — ideale per un agente IA che redige traduzioni, o per un passaggio di CI che registra le nuove chiavi trovate nel codice sorgente. Concedi gli ambiti quando crei il token nella scheda API:
| Ambito | Consente |
|---|---|
key:create |
Creare chiavi di traduzione |
key:edit |
Rinominare una chiave / cambiarne il namespace o la descrizione |
translation:edit |
Impostare/sovrascrivere i valori delle traduzioni |
language:manage |
Creare automaticamente una lingua di destinazione mancante durante l'invio |
Endpoint#
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)
Identifica la chiave tramite il suo percorso puntato key attuale oppure tramite keyId;
gli altri campi (name, namespace, description) sono i nuovi valori, e quelli che ometti
mantengono il valore corrente. L'aggiornamento avviene per id, quindi traduzioni, stati e
commenti della chiave sopravvivono a una rinomina — niente elimina-e-ricrea.
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 richiede anche key:create; inviare verso una lingua non ancora esistente
richiede language:manage (altrimenti la richiesta viene rifiutata con
400 Unknown locale). Le scritture da token compaiono nel feed delle attività del progetto,
attribuite al nome del token.
CLI di invio#
È incluso cli/lantern-push.mjs (senza dipendenze, speculare alla CLI di download):
# 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
Errori: 401 (token non valido), 403 (ambito mancante, o il token non corrisponde allo
slug), 400 (corpo non valido / lingua sconosciuta), 409 (la chiave esiste già).
Usare Lantern da un agente IA (MCP)#
Lantern ospita un server MCP perché un agente IA (Claude Code / Claude Desktop, ecc.) possa leggere e scrivere le traduzioni di un progetto in modo conversazionale — senza collante su misura, niente da installare. Sono gli ambiti del token a decidere cosa può fare l'agente (un token in sola lettura può leggere, ma gli strumenti di scrittura restituiscono 403).
Crea un token lk_ nella scheda API del progetto, poi punta il tuo client MCP
sull'endpoint:
{
"mcpServers": {
"lantern": {
"url": "https://lantern.abyss-inn.ch/api/v1/mcp",
"headers": { "Authorization": "Bearer lk_…" }
}
}
}
Il token identifica già il progetto, quindi non c'è altro da configurare.
Strumenti esposti all'agente:
| Strumento | Cosa fa | Ambito richiesto |
|---|---|---|
list_projects |
I progetti a cui questo token può accedere | — |
list_languages |
Le lingue di un progetto (codice + nome visualizzato + predefinita) | — |
get_translations |
Leggere le traduzioni (locale?, nested?, format?) |
— |
create_key |
Creare una chiave | key:create |
set_translations |
Inserire/aggiornare i valori di una lingua (createMissingKeys?) |
translation:edit |
createMissingKeys richiede anche key:create; creare automaticamente una nuova lingua
richiede language:manage.
Un token lk_ è limitato a un solo progetto, quindi gli strumenti vi agiscono
automaticamente. Una connessione OAuth a livello di utente (sotto) può raggiungere tutti
i tuoi progetti, perciò i suoi strumenti accettano uno slug project — chiama prima
list_projects, poi passa project agli altri. I permessi vengono riverificati progetto per
progetto a ogni chiamata, quindi l'agente non potrà mai fare su un progetto più di quanto puoi
farci tu.
Connettersi con OAuth (accedere invece di incollare un token)#
Se il tuo client MCP supporta OAuth (i connettori di Claude, molti agenti da IDE), puoi
connetterti accedendo invece di creare e incollare un token lk_. Punta il client sullo
stesso URL senza intestazione Authorization:
{
"mcpServers": {
"lantern": { "url": "https://lantern.abyss-inn.ch/api/v1/mcp" }
}
}
Il client individua automaticamente il server di autorizzazione di Lantern, apre un browser per
l'accesso e mostra una schermata di consenso che conferma che l'agente agirà come te, su
tutti i tuoi progetti. Una sola autorizzazione copre ogni progetto che puoi vedere (l'agente
ne sceglie uno per chiamata tramite l'argomento project); puoi concedere solo permessi che
possiedi tu stesso, e ognuno viene riverificato per progetto al momento della chiamata.
L'accesso è legato al tuo account — revocalo quando vuoi accedendo altrove o contattando chi
possiede lo spazio di lavoro. Lantern implementa lo standard OAuth 2.1 (PKCE, registrazione
dinamica dei client, refresh token a rotazione), quindi funziona qualsiasi client MCP conforme
alle specifiche.
Il metodo con token lk_ descritto sopra continua a funzionare invariato: è la scelta giusta
per CLI, script e CI, dove non c'è un browser per completare un accesso.
In CI (GitHub Actions)#
Aggiungi LANTERN_TOKEN come segreto del repository, poi scarica prima della 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
(Oppure richiama lo script di download del tuo linguaggio, qui sopra.) La build includerà quindi i file aggiornati.
Collegare i file a una libreria i18n#
Il JSON annidato corrisponde a ciò che si aspetta la maggior parte delle librerie i18n, per esempio:
- JS/TS:
i18next(resources),react-intl,vue-i18n,next-intl - Python: carica il JSON e cerca per chiave puntata (oppure output piatto: togli
nested=1) - C#: collegalo a sorgenti JSON per
IStringLocalizer, oppure leggilo direttamente - Rust:
fluent/rust-i18n(probabilmente preferirai l'output piatto — omettinested=1)
Se la tua libreria vuole chiavi piatte ("home.title"), togli nested=1 dall'URL.
Unity (Unity Localization)#
Per Unity non collegare il JSON grezzo: usa il pacchetto di
Localization ufficiale di
Unity come runtime (Locale, String Table, LocalizedString, supporto TMP) e lascia che sia
Lantern ad alimentarlo.
Il pacchetto lantern-unity è un ponte solo
per l'editor: un pulsante Window ▸ Lantern ▸ Pull Translations importa un progetto
(tramite l'output format=csv dell'API di lettura) direttamente in una String Table
Collection, con un clic — senza file da gestire a mano. Installalo dal Package Manager con
Add package from git URL…:
https://github.com/Rabzizz/lantern-unity.git#v0.1.0
com.unity.localization è una dipendenza, quindi si installa automaticamente. Usa un token
lk_ in sola lettura; viene salvato per macchina in EditorPrefs e non finisce mai nella
tua build. Per ora è in sola lettura (Lantern → Unity).
Niente pacchetto, solo CSV? Unity Localization può importare direttamente il CSV di
Lantern: salva …/translations?format=csv in un file, aggiungi l'estensione CSV alla tua
String Table Collection, puntala al file e premi Import. Il pacchetto non fa che
automatizzare questo giro.