Back to all docs

BeMyWords with Astro

A complete integration guide for Astro static sites. This pattern runs in production today — tried, not theoretical.

Shape of the integration:

  1. At build time, fetch translations for every language from BeMyWords into JSON files.
  2. In Astro pages, call t(key, fallback) — the fallback is your English master string; the lookup returns the translated value for the current locale.
  3. Optionally, run push-english to 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.