Back to all docs

BeMyWords with Android

Integration guide for Android apps using the standard res/values/strings.xml format.

Shape of the integration:

  1. At build time, fetch translations from BeMyWords and convert to strings.xml under res/values-<lang>/.
  2. Use R.string.<name> or getString(R.string.<name>) as normal.
  3. Optionally push your default strings.xml to BeMyWords as the English master.

Prerequisites

  • An Android Studio project with at least app/src/main/res/values/strings.xml.
  • A BeMyWords workspace, project, namespace, API key.
  • Node.js available in your build environment (the example uses Node; any scripting language works).

1. Environment variables

Locally in .env (gitignored) at project root; in CI, via your CI's secret store:

BEMYWORDS_BASE_URL=https://www.bemywords.no
BEMYWORDS_PROJECT_ID=<your-project-uuid>
BEMYWORDS_API_TOKEN=<your-api-token>
BEMYWORDS_NAMESPACE=android

2. Key naming convention

Android strings.xml requires names that match [a-z][a-z0-9_]*. BeMyWords allows dots and dashes — you need a convention.

Recommendation: use dots in BeMyWords (home.hero.title) and convert to underscores for Android (home_hero_title). Set and enforce this convention across your team before you start.

3. Fetch + convert script

scripts/fetch-translations.mjs:

import { writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const RES_DIR = join(__dirname, "..", "app", "src", "main", "res");

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 || "android";

// BeMyWords lang → Android values-<qualifier>
// Default (English) goes in values/, not values-en/
const LANGUAGES = {
  en: "values",
  nb: "values-nb",
  "pt-BR": "values-pt-rBR",
};

function toAndroidName(key) {
  // Dots + dashes → underscores. Normalize to [a-z][a-z0-9_]*
  const name = key.replace(/[.\-]/g, "_").toLowerCase();
  if (!/^[a-z][a-z0-9_]*$/.test(name)) {
    console.warn(`  skipping invalid name for Android: ${key}`);
    return null;
  }
  return name;
}

function escapeXmlText(s) {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "\\\"")
    .replace(/'/g, "\\'")
    .replace(/\n/g, "\\n");
}

function toStringsXml(dict) {
  const lines = ['<?xml version="1.0" encoding="utf-8"?>'];
  lines.push('<!-- Generated by fetch-translations.mjs from BeMyWords. Do not edit. -->');
  lines.push("<resources>");
  for (const [key, value] of Object.entries(dict).sort()) {
    const name = toAndroidName(key);
    if (!name) continue;
    lines.push(`    <string name="${name}">${escapeXmlText(value)}</string>`);
  }
  lines.push("</resources>");
  return lines.join("\n") + "\n";
}

async function fetchLanguage(bwLang) {
  const url = `${BASE_URL}/api/${PROJECT_ID}/latest/${bwLang}/${NAMESPACE}`;
  const res = await fetch(url, {
    headers: { Authorization: `Token token=${API_TOKEN}` },
  });
  if (!res.ok) throw new Error(`${bwLang}: HTTP ${res.status}`);
  return res.json();
}

async function main() {
  if (!PROJECT_ID || !API_TOKEN) {
    console.warn("BEMYWORDS_PROJECT_ID or BEMYWORDS_API_TOKEN not set.");
    return;
  }

  for (const [bwLang, valuesDir] of Object.entries(LANGUAGES)) {
    const data = await fetchLanguage(bwLang);
    const outDir = join(RES_DIR, valuesDir);
    mkdirSync(outDir, { recursive: true });
    const outPath = join(outDir, "strings.xml");
    writeFileSync(outPath, toStringsXml(data), "utf-8");
    console.log(`  [${bwLang}] ${Object.keys(data).length} keys → ${outPath}`);
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

4. Hook into the Gradle build

Edit app/build.gradle (or build.gradle.kts):

tasks.register("fetchTranslations", Exec) {
  workingDir = rootDir
  commandLine "node", "scripts/fetch-translations.mjs"
}

preBuild.dependsOn("fetchTranslations")

Kotlin DSL:

tasks.register<Exec>("fetchTranslations") {
    workingDir = rootDir
    commandLine("node", "scripts/fetch-translations.mjs")
}

tasks.named("preBuild") { dependsOn("fetchTranslations") }

Alternatively, run the script manually or from CI before ./gradlew build.

5. Use in code

val title = getString(R.string.home_hero_title)
val greeting = getString(R.string.greeting, userName)

Android positional format specifiers (%1$s, %2$d) pass through BeMyWords verbatim. Enable Android-aware placeholder validation in your BeMyWords project settings.

6. (Optional) Push English to BeMyWords

Parse app/src/main/res/values/strings.xml and PUT to BeMyWords:

import { readFileSync } from "node:fs";

const path = join(RES_DIR, "values", "strings.xml");
const xml = readFileSync(path, "utf-8");

const re = /<string\s+name="([^"]+)">([\s\S]*?)<\/string>/g;
const translations = {};
let match;
while ((match = re.exec(xml)) !== null) {
  const androidName = match[1];
  const rawValue = match[2];
  const value = rawValue
    .replace(/\\'/g, "'")
    .replace(/\\"/g, '"')
    .replace(/\\n/g, "\n")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&");
  // Reverse the underscore convention if you want dots in BeMyWords:
  // const key = androidName.replace(/_/g, "."); // only if your naming round-trips
  const key = androidName;
  translations[key] = value;
}

const url = `${BASE_URL}/api/overwrite/${PROJECT_ID}/latest/en/${NAMESPACE}`;
await fetch(url, { method: "PUT", headers: { Authorization: `Token token=${API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ translations }) });

Note the irreversibility concern: if you're converting BeMyWords dots → Android underscores on fetch, you can't trivially reverse on push unless your naming is strictly foo_bar_baz with no collisions. Either:

  • Keep underscore names in BeMyWords (simplest — no conversion either direction).
  • Keep dot names in BeMyWords AND maintain a mapping file in the script.

Pluralization

Android uses <plurals> for pluralization:

<plurals name="apples">
    <item quantity="one">%d apple</item>
    <item quantity="other">%d apples</item>
</plurals>

Store plural variants in BeMyWords as dot-keyed siblings (apples.one, apples.other) and write a second generator pass that emits <plurals> blocks. This is not covered by the basic script above.


API reference

See Astro integration guide.

Troubleshooting

AAPT: error: Invalid file name: your values-<qualifier> folder doesn't match Android conventions. Norwegian is values-nb, Brazilian Portuguese is values-pt-rBR (region prefixed with r). Reference.

error: resource string/<name> not found: the BeMyWords key got filtered out by toAndroidName (starts with digit, contains uppercase, etc.). Rename the key in BeMyWords to fit Android conventions.

HTML entities double-escaped: if you have HTML in your translations, make sure escapeXmlText only runs once. Android's CDATA support may be easier for strings that are unavoidably full of HTML.