BeMyWords with iOS (.strings)
Integration guide for iOS and macOS apps using Apple's standard Localizable.strings format.
Shape of the integration:
- At build time (or periodically), fetch translations from BeMyWords and convert to
.stringsfiles per language under<Language>.lproj/. - Use
NSLocalizedStringin Swift as normal. - Optionally, push
Base.lproj/Localizable.stringsto 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
.lprojdirectory (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
.stringsdictfrom 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
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.