Diese Integrationsanleitung ist nur auf Englisch verfügbar.
BeMyWords with Android
Integration guide for Android apps using the standard res/values/strings.xml format.
Shape of the integration:
- At build time, fetch translations from BeMyWords and convert to
strings.xmlunderres/values-<lang>/. - Use
R.string.<name>orgetString(R.string.<name>)as normal. - Optionally push your default
strings.xmlto 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.org
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.org";
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.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(/</g, "<")
.replace(/>/g, ">")
.replace(/&/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
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.
Was kostet BeMyWords?
Kostenloser Start: 5.000 gespeicherte Wörter, 1 Website, keine Karte. Bezahlte Tarife beginnen bei 199 kr pro Monat – 1.990 kr pro Jahr – und werden nach gespeicherten Wörtern berechnet – die Quelle plus jede Übersetzung davon – niemals nach Sitzplätzen, Seitenaufrufen oder einer Begrenzung der Anzahl der Sprachen, die Sie betreiben. Bringen Sie Ihren eigenen KI-Schlüssel mit und es kostet pauschal 199 kr pro Monat ohne Wortlimit.
Welche Preisstufe das ist, hängt davon ab, wie viele Wörter Ihre Website enthält. Fügen Sie Ihre Adresse ein und wir lesen sie aus und sagen es Ihnen.
Jeder Tarif und was er enthält Größe Ihrer Website ermittelnInstalliere es auf dem Projekt, über das du gerade gelesen hast.
Der kostenlose Tarif reicht aus, um eine Landing-Page zu übersetzen und die Ausgabe selbst zu beurteilen. Keine Karte erforderlich.
Kostenlos starten