Back to all docs

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.