Lantern est la source de vérité de vos traductions. Votre application conserve des
fichiers de locale locaux (locales/en.json, locales/fr.json, …) et les garde
synchronisés en les récupérant depuis l'API de lecture — en général dans une étape de
build ou de déploiement.
- URL de base :
https://lantern.abyss-inn.ch - Authentification : un jeton d'API créé dans l'onglet API du projet (un jeton par projet). Les jetons sont en lecture seule par défaut ; accorder des permissions d'écriture permet à un agent IA ou à votre CI de renvoyer clés et traductions (voir Écriture inverse)
- Point d'accès :
GET /api/v1/projects/<slug>/translations
Le point d'accès#
GET /api/v1/projects/<slug>/translations
Authorization: Bearer lk_…
| Paramètre | Valeurs | Signification |
|---|---|---|
format |
json (défaut), csv |
Format de sortie |
locale |
un code de locale (ex. en) |
Une seule langue. Omettez-le pour toutes |
nested |
1 |
JSON imbriqué ({"home":{"title":…}}) au lieu de plat ({"home.title":…}) |
Réponses
?locale=en&nested=1→ une seule langue :{ "home": { "title": "Welcome" } }- (sans
locale)?nested=1→ toutes les langues, indexées par locale (idéal pour synchroniser des fichiers) :{ "en": { "home": { "title": "Welcome" } }, "fr": { "home": { "title": "Bienvenue" } } } format=csv→ une colonnekeyplus une colonne par locale.
Erreurs : 401 (jeton manquant ou invalide), 403 (le jeton ne correspond pas au slug),
400 (locale inconnue / format invalide), 404 (projet introuvable).
Pour lister les langues du projet avec leurs noms d'affichage (l'appel ci-dessus n'expose que les codes de locale), utilisez :
GET /api/v1/projects/<slug>/languages
Authorization: Bearer lk_…
→ 200 [ { "locale": "en", "name": "English", "isDefault": true }, … ]
Sécurité des jetons#
- Le jeton est en lecture seule et limité à un seul projet. Stockez-le dans une
variable d'environnement / un secret de CI (par exemple
LANTERN_TOKEN) — ne le committez jamais. - Pour le renouveler : révoquez-le dans l'onglet API et créez-en un nouveau.
Stratégies de synchronisation#
- Récupération au build (recommandé). Lancez une étape de pull avant votre build afin
que les fichiers livrés soient toujours à jour. Vous pouvez mettre le dossier
locales/généré dans.gitignoreou le committer — comme vous préférez. - Récupération à l'exécution + cache. Les serveurs de longue durée peuvent récupérer les fichiers au démarrage et les rafraîchir périodiquement. Mettez-les en cache en mémoire et retombez sur la dernière copie valide en cas d'erreur.
- Planifiée. Pour les applications qui ne sont pas redéployées, planifiez le pull avec cron puis rechargez.
Code#
Chaque exemple ci-dessous récupère toutes les locales en une seule requête (?nested=1)
et écrit locales/<locale>.json. Pour ne récupérer qu'une langue, ajoutez ?locale=en et
n'écrivez qu'un fichier. Définissez LANTERN_TOKEN dans votre environnement et remplacez
my-project par votre slug.
Shell (curl + jq) — universel#
#!/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)#
Une CLI prête à l'emploi est fournie — pour les projets JS/TS, c'est le plus simple :
LANTERN_TOKEN=lk_… node cli/lantern-pull.mjs \
--url https://lantern.abyss-inn.ch --project my-project --out locales
Équivalent en ligne :
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 (bibliothèque standard, sans dépendances)#
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";
}
Écriture inverse — envoyer des clés et des traductions#
La synchronisation en lecture est à sens unique (Lantern → vos fichiers). Un jeton doté de permissions d'écriture peut aussi pousser dans l'autre sens — idéal pour un agent IA qui rédige des traductions, ou pour une étape de CI qui enregistre les nouvelles clés trouvées dans le code source. Accordez les portées à la création du jeton, dans l'onglet API :
| Portée | Autorise |
|---|---|
key:create |
Créer des clés de traduction |
key:edit |
Renommer une clé / changer son espace de noms ou sa description |
translation:edit |
Définir ou écraser des valeurs de traduction |
language:manage |
Créer automatiquement une langue cible manquante lors d'un envoi |
Points d'accès#
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)
Identifiez la clé par son chemin pointé key actuel ou par keyId ; les autres champs
(name, namespace, description) sont les nouvelles valeurs, et ceux que vous omettez
conservent leur valeur actuelle. La mise à jour se fait par identifiant, donc les
traductions, statuts et commentaires de la clé survivent à un renommage — pas de
suppression-recréation.
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 requiert aussi key:create ; envoyer vers une locale qui n'existe pas
encore requiert language:manage (sinon la requête est rejetée avec 400 Unknown locale).
Les écritures par jeton apparaissent dans le flux d'activité du projet, attribuées au nom
du jeton.
CLI d'envoi#
Une CLI cli/lantern-push.mjs est fournie (sans dépendances, symétrique de celle de
lecture) :
# 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
Erreurs : 401 (jeton invalide), 403 (portée manquante, ou le jeton ne correspond pas
au slug), 400 (corps invalide / locale inconnue), 409 (la clé existe déjà).
Utiliser Lantern depuis un agent IA (MCP)#
Lantern héberge un serveur MCP pour qu'un agent IA (Claude Code / Claude Desktop, etc.) puisse lire et écrire les traductions d'un projet de façon conversationnelle — sans colle sur mesure, rien à installer. Ce sont les portées du jeton qui décident de ce que l'agent peut faire (un jeton en lecture seule peut lire, mais les outils d'écriture renvoient 403).
Créez un jeton lk_ dans l'onglet API du projet, puis pointez votre client MCP sur le
point d'accès :
{
"mcpServers": {
"lantern": {
"url": "https://lantern.abyss-inn.ch/api/v1/mcp",
"headers": { "Authorization": "Bearer lk_…" }
}
}
}
Le jeton identifie déjà le projet : il n'y a rien d'autre à configurer.
Outils exposés à l'agent :
| Outil | Rôle | Portée requise |
|---|---|---|
list_projects |
Les projets accessibles avec ce jeton | — |
list_languages |
Les langues d'un projet (locale + nom d'affichage + défaut) | — |
get_translations |
Lire les traductions (locale?, nested?, format?) |
— |
create_key |
Créer une clé | key:create |
set_translations |
Insérer/mettre à jour les valeurs d'une locale (createMissingKeys?) |
translation:edit |
createMissingKeys requiert aussi key:create ; créer automatiquement une nouvelle locale
requiert language:manage.
Un jeton lk_ est limité à un projet : les outils agissent donc automatiquement sur celui-ci.
Une connexion OAuth à l'échelle de l'utilisateur (ci-dessous) peut atteindre tous vos
projets, ses outils prennent donc un slug de project — appelez d'abord list_projects, puis
passez project aux autres. Les permissions sont revérifiées projet par projet à chaque
appel : l'agent ne peut jamais faire sur un projet plus que ce que vous pouvez y faire.
Se connecter avec OAuth (se connecter au lieu de coller un jeton)#
Si votre client MCP prend en charge OAuth (les connecteurs de Claude, de nombreux agents
d'IDE), vous pouvez vous connecter en vous authentifiant plutôt qu'en créant et collant un
jeton lk_. Pointez le client sur la même URL, sans en-tête Authorization :
{
"mcpServers": {
"lantern": { "url": "https://lantern.abyss-inn.ch/api/v1/mcp" }
}
}
Le client découvre automatiquement le serveur d'autorisation de Lantern, ouvre un navigateur
pour la connexion et affiche un écran de consentement confirmant que l'agent agira en
votre nom, sur l'ensemble de vos projets. Une seule autorisation couvre tous les projets que
vous pouvez voir (l'agent en choisit un par appel via l'argument project) ; vous ne pouvez
accorder que des permissions que vous détenez vous-même, et chacune est revérifiée par projet
au moment de l'appel. L'accès est lié à votre compte — révoquez-le à tout moment en vous
connectant ailleurs ou en contactant le propriétaire de l'espace de travail. Lantern
implémente le standard OAuth 2.1 (PKCE, enregistrement dynamique des clients, jetons de
rafraîchissement rotatifs) : tout client MCP conforme à la spécification fonctionne.
La méthode par jeton lk_ ci-dessus continue de fonctionner telle quelle — c'est le bon choix
pour les CLI, les scripts et la CI, là où aucun navigateur ne peut compléter une connexion.
En CI (GitHub Actions)#
Ajoutez LANTERN_TOKEN comme secret du dépôt, puis récupérez les fichiers avant le 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
(Ou appelez le script de récupération de votre langage, ci-dessus.) Le build embarque alors les fichiers frais.
Brancher les fichiers sur une bibliothèque i18n#
Le JSON imbriqué correspond à ce qu'attendent la plupart des bibliothèques i18n, par exemple :
- JS/TS :
i18next(resources),react-intl,vue-i18n,next-intl - Python : chargez le JSON et faites la recherche par clé pointée (ou sortie plate :
retirez
nested=1) - C# : branchez-le sur des sources JSON
IStringLocalizer, ou lisez-le directement - Rust :
fluent/rust-i18n(vous préférerez sans doute la sortie plate — ometteznested=1)
Si votre bibliothèque veut des clés plates ("home.title"), retirez nested=1 de l'URL.
Unity (Unity Localization)#
Pour Unity, ne branchez pas le JSON brut : utilisez le package de
Localization officiel
d'Unity comme runtime (Locales, String Tables, LocalizedString, prise en charge de TMP) et
laissez Lantern l'alimenter.
Le package lantern-unity est une passerelle
éditeur uniquement : un bouton Window ▸ Lantern ▸ Pull Translations importe un projet (via
la sortie format=csv de l'API de lecture) directement dans une String Table Collection,
en un clic — sans fichiers à manipuler. Installez-le depuis le Package Manager avec Add
package from git URL… :
https://github.com/Rabzizz/lantern-unity.git#v0.1.0
com.unity.localization est une dépendance, elle s'installe donc automatiquement. Utilisez un
jeton lk_ en lecture seule ; il est stocké par machine dans EditorPrefs et n'est jamais
inclus dans votre build. La passerelle est en lecture seule (Lantern → Unity) pour l'instant.
Pas de package, juste du CSV ? Unity Localization peut importer directement le CSV de
Lantern : enregistrez …/translations?format=csv dans un fichier, ajoutez l'extension CSV
à votre String Table Collection, pointez-la sur le fichier et lancez Import. Le package ne
fait qu'automatiser cet aller-retour.