Back to all docs

This integration guide is available in English only.

BeMyWords with iOS (.strings)

Integration guide for iOS and macOS apps using Apple's standard Localizable.strings format.

Shape of the integration:

  1. At build time (or periodically), fetch translations from BeMyWords and convert to .strings files per language under <Language>.lproj/.
  2. Use NSLocalizedString in Swift as normal.
  3. Optionally, push Base.lproj/Localizable.strings to BeMyWords as the English master source.

This guide assumes the classic .strings format. If you're on Xcode 15+ with String Catalogs (.xcstrings), the principles are the same but you'd convert to that JSON format on the client side — out of scope here.


Prerequisites

  • An Xcode project with at least one .lproj directory (Base or en).
  • A BeMyWords workspace, project, namespace, and API key.
  • Node.js available in your build environment (used for the fetch script — swap for any language you prefer).

1. Environment variables

Typically stored in a build-env file or set in your CI. Locally you can keep them in a gitignored .env at the project root:

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

2. 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));
// Adjust to where your .lproj directories live
const PROJECT_ROOT = join(__dirname, "..", "MyApp");

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

// Map BeMyWords language codes to iOS locale identifiers.
// BeMyWords: en, nb, pt-BR   →   iOS: en, nb, pt-BR
const LANGUAGES = { en: "en", nb: "nb", "pt-BR": "pt-BR" };

function escapeStringsValue(s) {
  return s
    .replace(/\\/g, "\\\\")
    .replace(/"/g, '\\"')
    .replace(/\n/g, "\\n")
    .replace(/\r/g, "\\r")
    .replace(/\t/g, "\\t");
}

function toStringsFormat(dict) {
  const lines = ['/* Generated by fetch-translations.mjs from BeMyWords. Do not edit. */', ''];
  for (const [key, value] of Object.entries(dict).sort()) {
    lines.push(`"${escapeStringsValue(key)}" = "${escapeStringsValue(value)}";`);
  }
  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, iosLocale] of Object.entries(LANGUAGES)) {
    const data = await fetchLanguage(bwLang);
    const lprojDir = join(PROJECT_ROOT, `${iosLocale}.lproj`);
    mkdirSync(lprojDir, { recursive: true });
    const stringsPath = join(lprojDir, "Localizable.strings");
    writeFileSync(stringsPath, toStringsFormat(data), "utf-8");
    console.log(`  [${iosLocale}] ${Object.keys(data).length} keys → ${stringsPath}`);
  }
}

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

3. Wire into Xcode build

In Build Phases → New Run Script Phase (place it before "Copy Bundle Resources"):

if [ -f "$SRCROOT/.env" ]; then
  set -a; source "$SRCROOT/.env"; set +a
fi
cd "$SRCROOT"
node scripts/fetch-translations.mjs

Uncheck "Based on dependency analysis" so it always runs.

Alternatively, run it manually or via CI before xcodebuild:

node scripts/fetch-translations.mjs
xcodebuild build ...

4. Use in Swift

let title = NSLocalizedString("home.hero.title", value: "Localization for solo devs and small teams", comment: "Home hero title")

let greeting = String(
  format: NSLocalizedString("greeting", value: "Hello, %@!", comment: ""),
  userName
)

BeMyWords stores iOS format specifiers (%@, %d) verbatim. Use placeholder validation in BeMyWords project settings.

5. (Optional) Push English to BeMyWords

Parse your Base or en Localizable.strings and PUT to BeMyWords:

import { readFileSync } from "node:fs";

const path = join(PROJECT_ROOT, "en.lproj", "Localizable.strings");
const text = readFileSync(path, "utf-8");

// Naive parser — good enough for well-formatted .strings files
const re = /"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)"\s*;/g;
const translations = {};
let match;
while ((match = re.exec(text)) !== null) {
  const key = match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
  const value = match[2].replace(/\\"/g, '"').replace(/\\\\/g, "\\").replace(/\\n/g, "\n");
  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 }),
});

Pluralization

iOS uses .stringsdict files for plurals (XML). That conversion is not covered by this basic script — if you rely heavily on pluralization:

  • Store plural variants in BeMyWords as dot-keyed siblings (apples.one, apples.other).
  • Write a second generator pass that produces .stringsdict from those keys.
  • Or switch to String Catalogs (.xcstrings) which handle plurals natively in JSON.

For small-team apps with few plural strings, it's often simpler to branch in code:

let message = count == 1
  ? NSLocalizedString("apple.singular", value: "1 apple", comment: "")
  : String(format: NSLocalizedString("apple.plural", value: "%d apples", comment: ""), count)

API reference

See Astro integration guide.

Troubleshooting

Strings show as keys in the app: Xcode didn't rebuild the .strings files into the bundle. Clean build folder (Shift + Cmd + K) and rebuild.

Quotes / newlines render as literal \" in the UI: the escape function in the script didn't handle the character. Check escapeStringsValue — Apple's .strings format requires \\, \", \n, \r, \t to be backslash-escaped.

Missing translations for a region: BeMyWords uses nb but iOS uses nb, no, or nb-NO depending on language setting. Either add a nb-NO.lproj fallback or rely on iOS's language-tag resolution.

Base vs en.lproj confusion: if you have both Base.lproj/Localizable.strings and en.lproj/Localizable.strings, Xcode may prefer one unpredictably. Pick one as the canonical English location and delete the other; write the fetch script to populate whichever you keep.