BeMyWords with Astro
A complete integration guide for Astro static sites. This pattern runs in production today — tried, not theoretical.
Shape of the integration:
- At build time, fetch translations for every language from BeMyWords into JSON files.
- In Astro pages, call
t(key, fallback)— the fallback is your English master string; the lookup returns the translated value for the current locale. - Optionally, run
push-englishto sync your English master strings to BeMyWords (reverse direction).
No runtime dependency on BeMyWords — the site builds to static HTML with the translated strings already baked in.
Prerequisites
- An Astro project (we assume
[country]/[lang]/routing, but the pattern works with any locale routing you prefer). - A BeMyWords workspace with a project and at least one namespace.
- A BeMyWords API key scoped to that project (create in Project settings → API keys).
1. Environment variables
Create .env in your project root:
BEMYWORDS_BASE_URL=https://www.bemywords.no
BEMYWORDS_PROJECT_ID=<your-project-uuid>
BEMYWORDS_API_TOKEN=<your-api-token>
BEMYWORDS_NAMESPACE=www
Add .env to .gitignore if it isn't already. Your CI/deployment platform needs the same variables set.
2. Fetch translations at build time
Create scripts/fetch-translations.mjs:
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
// Load .env for local development
try {
const envFile = readFileSync(new URL("../.env", import.meta.url), "utf-8");
for (const line of envFile.split("\n")) {
const match = line.match(/^\s*([^#=]+?)\s*=\s*(.*?)\s*$/);
if (match && !process.env[match[1]]) process.env[match[1]] = match[2];
}
} catch {}
const __dirname = dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = join(__dirname, "..", "src", "data", "translations");
const BASE_URL = process.env.BEMYWORDS_BASE_URL || "https://www.bemywords.no";
const PROJECT_ID = process.env.BEMYWORDS_PROJECT_ID || "";
const API_TOKEN = process.env.BEMYWORDS_API_TOKEN || "";
const NAMESPACE = process.env.BEMYWORDS_NAMESPACE || "www";
// The languages your site supports
const LANGUAGES = ["en", "nb"];
async function fetchLanguage(lang) {
const url = `${BASE_URL}/api/${PROJECT_ID}/latest/${lang}/${NAMESPACE}`;
const response = await fetch(url, {
headers: { Authorization: `Token token=${API_TOKEN}` },
});
if (!response.ok) {
console.warn(` [${lang}] HTTP ${response.status} — using empty translations`);
return {};
}
const data = await response.json();
console.log(` [${lang}] fetched ${Object.keys(data).length} keys`);
return data;
}
async function main() {
console.log("Fetching translations from BeMyWords...");
mkdirSync(OUTPUT_DIR, { recursive: true });
if (!PROJECT_ID || !API_TOKEN) {
console.warn("BEMYWORDS_PROJECT_ID or BEMYWORDS_API_TOKEN not set — writing empty files.");
for (const lang of LANGUAGES) {
writeFileSync(join(OUTPUT_DIR, `${lang}.json`), "{}\n");
}
return;
}
await Promise.all(
LANGUAGES.map(async (lang) => {
const data = await fetchLanguage(lang);
writeFileSync(join(OUTPUT_DIR, `${lang}.json`), JSON.stringify(data, null, 2) + "\n");
})
);
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
Add src/data/translations/*.json to .gitignore — these are regenerated on every build.
3. Wire up a translator helper
Create src/lib/i18n.ts:
import enTranslations from "../data/translations/en.json";
import nbTranslations from "../data/translations/nb.json";
const TRANSLATIONS: Record<string, Record<string, string>> = {
en: enTranslations,
nb: nbTranslations,
};
export type TranslatorFn = (key: string, fallback: string) => string;
export function createTranslator(lang: string): TranslatorFn {
const dict = TRANSLATIONS[lang] ?? {};
return (key, fallback) => dict[key] ?? fallback;
}
4. Use t() in pages
In any .astro file:
---
import { createTranslator } from "@/lib/i18n";
const { lang } = Astro.params; // or however you determine the current lang
const t = createTranslator(lang);
---
<h1>{t("home.hero.title", "Localization for solo devs and small teams")}</h1>
<p>{t("home.hero.subtitle", "Cheap, simple, developer-first.")}</p>
The fallback is your English master. When a key is missing in the current language's JSON, the fallback is shown. This means:
- Your English page always renders correctly (fallbacks are English).
- Non-English pages gracefully fall back to English for any untranslated key.
- New strings land in the code first, then get translated in BeMyWords later.
5. Wire the fetch into your build
In package.json:
{
"scripts": {
"dev": "node scripts/fetch-translations.mjs && astro dev",
"build": "node scripts/fetch-translations.mjs && astro build"
}
}
Now every npm run dev and npm run build pulls the latest translations.
6. (Optional) Push English master to BeMyWords
To send your English master strings (the fallbacks in your t() calls) to BeMyWords so translators have them:
Create scripts/push-english.mjs:
import { readFileSync, readdirSync } from "node:fs";
import { join, dirname, extname } from "node:path";
import { fileURLToPath } from "node:url";
// Load .env
try {
const envFile = readFileSync(new URL("../.env", import.meta.url), "utf-8");
for (const line of envFile.split("\n")) {
const match = line.match(/^\s*([^#=]+?)\s*=\s*(.*?)\s*$/);
if (match && !process.env[match[1]]) process.env[match[1]] = match[2];
}
} catch {}
const BASE_URL = process.env.BEMYWORDS_BASE_URL;
const PROJECT_ID = process.env.BEMYWORDS_PROJECT_ID;
const API_TOKEN = process.env.BEMYWORDS_API_TOKEN;
const NAMESPACE = process.env.BEMYWORDS_NAMESPACE || "www";
// Your English master strings — keep this file as your source of truth.
// You can generate it from t() calls in source, or maintain by hand.
const translations = {
"home.hero.title": "Localization for solo devs and small teams",
"home.hero.subtitle": "Cheap, simple, developer-first.",
// ... rest of your keys
};
const url = `${BASE_URL}/api/overwrite/${PROJECT_ID}/latest/en/${NAMESPACE}`;
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Token token=${API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ translations }),
});
if (!response.ok) {
console.error(`Push failed: HTTP ${response.status}`);
process.exit(1);
}
console.log(`Pushed ${Object.keys(translations).length} English keys to BeMyWords.`);
Run with node scripts/push-english.mjs whenever you've added or changed master strings.
7. (Optional) Sync keys — remove hidden and keep source in sync
For a fuller workflow that tells BeMyWords which keys are still in use (everything else gets hidden) and syncs source-string changes, use the sync endpoint instead of overwrite. See the API reference below.
API reference
All endpoints require Authorization: Token token=<API_KEY> header. Base URL: https://www.bemywords.no.
| Verb | Path | Purpose |
|---|---|---|
GET |
/api/:project_id/latest/:lang/:namespace |
Fetch all translations for a language (used at build time) |
PUT |
/api/overwrite/:project_id/latest/:lang/:namespace |
Overwrite all translations for a language — body { translations: { key: value, ... } } |
PUT |
/api/sync/:project_id/latest/:lang/:namespace |
Sync canonical key map — unhides present keys, hides missing ones, updates source strings |
POST |
/api/missing/:project_id/latest/:lang/:namespace |
Create any keys that don't yet exist, with default values |
GET |
/api/stale/:project_id/latest/:namespace?lang=nb |
List keys whose source changed since the target-language translation |
GET |
/api/:project_id/changes/:lang/:namespace?since=<ISO> |
List translations that changed since a timestamp (for live-swap on static sites) |
See API docs in the dashboard for detailed request/response shapes.
Troubleshooting
HTTP 401: the API token doesn't match the project. Check BEMYWORDS_PROJECT_ID matches the project the token was issued for.
HTTP 404 on fetch: the namespace or language doesn't exist. Check the namespace key in Project settings and that the language is enabled in Project settings → Languages.
Empty translation files on build: the fetch script catches network errors and falls back to empty JSON. Check your build logs for [lang] HTTP 5xx or fetch error lines. Verify BEMYWORDS_BASE_URL is reachable from your build environment.
Build works locally, fails on deploy: the deploy platform probably doesn't have the env vars. Copy .env contents to your CI secrets (GitHub Actions, Vercel, Netlify, Cloudflare Pages all support this).
Reference implementation
This pattern is in production today. If you want a working example tailored to your stack, email support@skiwo.com — we'll send you a stripped-down reference repo.