Tillbaka till all dokumentation

Denna integrationsguide är endast tillgänglig på engelska.

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.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, "&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.

Vad kostar BeMyWords?

Gratis att börja: 5 000 ord lagrade, 1 webbplats, inget kort. Betalda nivåer börjar på 199 kr i månaden, eller 1 990 kr för ett år. De räknar orden du lagrar, originalet plus varje översättning av det, och aldrig personer, sidvisningar eller ett tak på hur många språk du kör. Ta med din egen AI-nyckel och det är 199 kr i månaden platt utan ordgräns.

Vilken nivå det är beror på hur många ord din webbplats innehåller. Klistra in din adress så läser vi den och berättar.

Varje nivå, och vad den innehåller Beräkna din webbplats storlek

Installera det på projektet du just läste om.

Den kostnadsfria nivån räcker för att översätta en landningssida och bedöma resultatet själv. Inget kort.

Starta gratis