Recipes
Working examples for the three integrations that cover most cases.
Fetch the newest bundle at runtime
The simplest possible integration: no token, no API call, no rate limit. Suitable when you want copy fixes to appear without a deploy.
const PROJECT_SLUG = "8Qw2ZrKt6Yb1Nc0P"; // from any download URL
/** Fetch the newest published translations for one language. */export async function loadTranslations(language) { const url = `https://cdn.localeo.app/p/${PROJECT_SLUG}/r/latest/${language}.json`; const res = await fetch(url); if (!res.ok) { throw new Error(`Localeo: ${language} bundle returned ${res.status}`); } return res.json();}package translations
import ( "encoding/json" "fmt" "net/http")
const projectSlug = "8Qw2ZrKt6Yb1Nc0P"
// Load fetches the newest published translations for one language.func Load(language string) (map[string]string, error) { url := fmt.Sprintf("https://cdn.localeo.app/p/%s/r/latest/%s.json", projectSlug, language)
res, err := http.Get(url) if err != nil { return nil, err } defer res.Body.Close()
if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("localeo: %s bundle returned %s", language, res.Status) }
var out map[string]string if err := json.NewDecoder(res.Body).Decode(&out); err != nil { return nil, err } return out, nil}Download every language at build time
Pulls the newest release’s metadata once, then writes one file per language into your source tree. This is the shape most build pipelines want: translations become static assets, and a failure fails the build rather than the app.
import { mkdir, writeFile } from "node:fs/promises";
const API = process.env.LOCALEO_API ?? "https://api-public.localeo.app/v1";const TOKEN = process.env.LOCALEO_TOKEN;const OUT_DIR = "src/translations";
if (!TOKEN) throw new Error("LOCALEO_TOKEN is not set");const headers = { Authorization: `Bearer ${TOKEN}` };
/** Fail loudly, and say something useful about why. */async function api(path) { const res = await fetch(`${API}${path}`, { headers }); if (res.status === 429) { const retry = res.headers.get("retry-after"); throw new Error(`Rate limited. Retry in ${retry}s.`); } if (!res.ok) { const { error } = await res.json().catch(() => ({})); throw new Error(`GET ${path} -> ${res.status}${error ? `: ${error}` : ""}`); } return res.json();}
// Newest published release. `items` is ordered newest-first.const { items } = await api("/releases?page_size=1");const [newest] = items;if (!newest) throw new Error("This project has no published releases yet.");
const release = await api(`/releases/${newest.id}`);console.log(`Localeo: ${release.tag} (${release.languages.length} languages)`);
await mkdir(OUT_DIR, { recursive: true });
await Promise.all( release.languages.map(async ({ code, download_urls }) => { // The download URL needs no Authorization header. const res = await fetch(download_urls.json); if (!res.ok) throw new Error(`${code}: bundle returned ${res.status}`); await writeFile(`${OUT_DIR}/${code}.json`, await res.text()); console.log(` ✓ ${code}`); }),);LOCALEO_TOKEN=localeo_… node scripts/pull-translations.mjsPin a release in CI
For builds that must be reproducible, resolve the release once and commit the identifier, rather than taking whatever is newest at build time.
- name: Pull translations env: LOCALEO_TOKEN: ${{ secrets.LOCALEO_TOKEN }} # Committed to the repo and bumped deliberately, so a rebuild of an old # commit produces the strings that commit shipped with. LOCALEO_RELEASE: 7bWq0R run: | set -euo pipefail
release=$(curl -sS --fail-with-body \ -H "Authorization: Bearer $LOCALEO_TOKEN" \ "https://api-public.localeo.app/v1/releases/$LOCALEO_RELEASE")
echo "$release" | jq -r '.tag as $tag | "Pinned to \($tag)"'
mkdir -p src/translations echo "$release" | jq -r '.languages[] | "\(.code)\t\(.download_urls.json)"' | while IFS=$'\t' read -r code url; do curl -sS --fail-with-body -o "src/translations/${code}.json" "$url" echo " ✓ ${code}" doneTo bump the pin, list releases, pick the one you want, and change LOCALEO_RELEASE:
curl -s "https://api-public.localeo.app/v1/releases?page_size=10" \ -H "Authorization: Bearer $LOCALEO_TOKEN" | jq -r '.items[] | "\(.id) \(.tag) \(.published_at)"'Handling 429 properly
A minimal client that honours Retry-After rather than guessing, and gives up rather than looping:
async function apiWithRetry(path, { headers, retries = 1 } = {}) { for (let attempt = 0; ; attempt++) { const res = await fetch(path, { headers }); if (res.status !== 429 || attempt >= retries) return res;
// The window is fixed, so Retry-After is exact — backing off by anything // less just fails again. const wait = Number(res.headers.get("retry-after") ?? 60); console.warn(`Localeo: rate limited, waiting ${wait}s`); await new Promise((r) => setTimeout(r, wait * 1000)); }}See Rate limits for why exponential backoff is the wrong shape here.