Retour à toute la documentation

Ce guide d'intégration est disponible en anglais uniquement.

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.

Combien coûte BeMyWords ?

Gratuit pour commencer : 5 000 mots stockés, 1 site, sans carte. Les forfaits payants commencent à 199 kr par mois — 1 990 kr pour un an — et sont comptés sur les mots stockés — la source plus chaque traduction — jamais sur les sièges, les pages vues ou un plafond sur le nombre de langues que vous utilisez. Apportez votre propre clé IA et c'est 199 kr par mois forfaitaire sans limite de mots.

La formule correspondante dépend du nombre de mots que contient votre site. Collez votre adresse et nous la lirons pour vous le dire.

Chaque forfait et ce qu'il contient Évaluer votre site

Installez-le sur le projet que vous venez de découvrir.

Le forfait gratuit suffit pour traduire une landing page et juger le résultat par vous-même. Aucune carte requise.

Commencer gratuitement