Skip to content

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();
}

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.

scripts/pull-translations.mjs
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}`);
}),
);
Terminal window
LOCALEO_TOKEN=localeo_… node scripts/pull-translations.mjs

Pin 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.

.github/workflows/build.yaml
- 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}"
done

To bump the pin, list releases, pick the one you want, and change LOCALEO_RELEASE:

Terminal window
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.