Intégrer Lantern (garder vos fichiers de traduction synchronisés)

Sur cette page

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 colonne key plus 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#

  1. 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 .gitignore ou le committer — comme vous préférez.
  2. 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.
  3. 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 — omettez nested=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.