You're shipping on iOS, Android, and React Native, and suddenly your translation workflow becomes a three-headed hydra. Each platform wants its strings in a different format, your translators are working from inconsistent source files, and you're manually copying changes between platforms. There's a better way.
Let's start with what we're dealing with. Each platform has its own idea of how translations should be structured, and ignoring these differences leads to broken apps and frustrated translators.
Android uses strings.xml with XML structure:
<!-- strings.xml -->
<resources>
<string name="welcome_message">Welcome to our app</string>
<string name="login_button">Log In</string>
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>
iOS uses .strings files with key-value pairs:
/* Localizable.strings */
"welcome_message" = "Welcome to our app";
"login_button" = "Log In";
React Native typically uses JSON:
{
"welcome_message": "Welcome to our app",
"login_button": "Log In",
"items_count": {
"one": "{{count}} item",
"other": "{{count}} items"
}
}
The format differences are just the beginning. Each platform handles pluralization differently, has different interpolation syntax, and supports different features. Your cross-platform strategy needs to account for these differences without creating a maintenance nightmare.
Establishing a Single Source of Truth
The key to managing cross-platform translations is establishing one authoritative source that can generate platform-specific outputs. This isn't about forcing all platforms to use the same format—it's about having one place where translators work and changes are made.
Option 1: JSON as the Master Format
JSON works well as a source format because it's readable, supports nested structures, and can represent most platform-specific concepts:
{
"welcome": {
"title": "Welcome to our app",
"subtitle": "Get started in seconds"
},
"auth": {
"login_button": "Log In",
"signup_button": "Sign Up",
"forgot_password": "Forgot your password?"
},
"items": {
"count": {
"_type": "plural",
"one": "{{count}} item",
"other": "{{count}} items"
}
}
}
Option 2: Platform-Agnostic Intermediate Format
Some teams create a custom format that captures platform-specific requirements without being tied to any single platform:
# translations.yml
strings:
welcome_message:
value: "Welcome to our app"
context: "Shown on first app launch"
login_button:
value: "Log In"
max_length: 20
platforms:
ios:
accessibility_label: "Sign in to your account"
plurals:
items_count:
one: "{{count}} item"
other: "{{count}} items"
android_zero: "No items" # Android-specific
The choice depends on your team's needs, but JSON tends to be the most practical for most teams since it requires less custom tooling.
Pluralization is where cross-platform string management gets interesting. Each platform has different rules and different ways of expressing plural forms.
Android supports six plural categories (zero, one, two, few, many, other) but uses them inconsistently across languages. iOS uses .stringsdict files with similar categories but different syntax. React Native typically relies on libraries like react-i18next or format-js.
Here's how to handle this in your source format:
{
"notifications": {
"unread_count": {
"_type": "plural",
"_rules": {
"android": {
"zero": "No new notifications",
"one": "{{count}} new notification",
"other": "{{count}} new notifications"
},
"ios": {
"zero": "No new notifications",
"one": "%d new notification",
"other": "%d new notifications"
},
"react_native": {
"zero": "No new notifications",
"one": "{{count}} new notification",
"other": "{{count}} new notifications"
}
}
}
}
}
Your build process then extracts the appropriate rules for each platform. This approach handles the reality that pluralization requirements can differ between platforms for the same logical string.
For more details on platform-specific pluralization challenges, check out our guides on React Native pluralization, iOS .stringsdict files, and Android plural handling.
Some UI copy needs to be different across platforms due to design differences, platform conventions, or technical constraints. Your string management system needs to support these variations without creating completely separate translation workflows.
Method 1: Platform Overrides
Define a default string and allow platform-specific overrides:
{
"navigation": {
"back_button": {
"default": "Back",
"ios": "< Back",
"android": "Back"
}
},
"errors": {
"network_error": {
"default": "Network connection failed. Please try again.",
"android": "No internet connection. Check your network settings.",
"_context": "Android users expect more specific network guidance"
}
}
}
Method 2: Conditional Strings
Some strings only apply to specific platforms:
{
"permissions": {
"camera_rationale": {
"_android_only": true,
"value": "This app needs camera access to scan QR codes",
"_context": "Required for Android permission rationale"
},
"location_always": {
"_ios_only": true,
"value": "Allow location access even when app is closed",
"_context": "iOS location permission dialog"
}
}
}
Your build process filters these appropriately, ensuring each platform only gets the strings it needs.
Build Process Integration and Automation
The magic happens in your build process. You need scripts that transform your single source of truth into platform-specific formats, ideally integrated into your CI/CD pipeline.
Basic Transformation Script (Node.js example):
// generate-strings.js
const fs = require('fs');
const path = require('path');
class StringGenerator {
constructor(sourceFile) {
this.strings = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
}
generateAndroid(outputPath) {
let xml = '<?xml version="1.0" encoding="utf-8"?>\n<resources>\n';
Object.entries(this.flattenStrings()).forEach(([key, value]) => {
if (value._type === 'plural') {
xml += this.generateAndroidPlural(key, value);
} else {
const androidValue = value.android || value.default || value;
xml += ` <string name="${key}">${this.escapeXml(androidValue)}</string>\n`;
}
});
xml += '</resources>\n';
fs.writeFileSync(path.join(outputPath, 'strings.xml'), xml);
}
generateiOS(outputPath) {
let strings = '';
Object.entries(this.flattenStrings()).forEach(([key, value]) => {
if (value._type !== 'plural') {
const iosValue = value.ios || value.default || value;
strings += `"${key}" = "${this.escapeStrings(iosValue)}";\n`;
}
});
fs.writeFileSync(path.join(outputPath, 'Localizable.strings'), strings);
}
generateReactNative(outputPath) {
const output = {};
Object.entries(this.flattenStrings()).forEach(([key, value]) => {
if (value._type === 'plural') {
output[key] = value.react_native || value._rules?.react_native;
} else {
output[key] = value.react_native || value.default || value;
}
});
fs.writeFileSync(
path.join(outputPath, 'translations.json'),
JSON.stringify(output, null, 2)
);
}
}
CI Integration:
# .github/workflows/localization.yml
name: Generate Platform Strings
on:
push:
paths: ['localization/source/**']
jobs:
generate-strings:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Generate platform strings
run: |
node scripts/generate-strings.js
- name: Commit generated files
run: |
git config --local user.email "[email protected]"
git add android/app/src/main/res/values/strings.xml
git add ios/App/Localizable.strings
git add src/i18n/translations.json
git commit -m "Auto-update platform strings" || exit 0
git push
This approach builds on the automation concepts covered in our CI/CD localization guide, extending them to handle cross-platform synchronization.
Several tools can help manage cross-platform string synchronization, each with different trade-offs:
Lokalise/Crowdin with Platform Exports: These translation management platforms can export to multiple formats from a single project. You upload your source format and download platform-specific files. Works well for teams already using these platforms.
Custom Build Tools: Many teams build their own transformation tools. This gives maximum control but requires maintenance. Start simple and add features as needed.
BabelEdit/i18n Manager: Desktop applications that can import/export multiple formats. Good for smaller teams that want a visual interface.
React Native Libraries: Tools like react-native-localize can consume the same translation files used by your native code, reducing duplication.
The key is picking tools that fit your existing workflow rather than forcing your team to adapt to new processes. If you're already using a particular translation management platform, extend it. If you're managing everything in code, build transformation scripts.
Validation and Quality Control
Cross-platform string management introduces new failure modes. Your validation process needs to catch platform-specific issues before they reach production.
Automated Validation Checks:
// validation-checks.js
function validateCrossPlatform(strings) {
const errors = [];
// Check for platform-specific interpolation consistency
Object.entries(strings).forEach(([key, value]) => {
if (value.android && value.ios) {
const androidPlaceholders = extractPlaceholders(value.android, 'android');
const iosPlaceholders = extractPlaceholders(value.ios, 'ios');
if (!placeholdersMatch(androidPlaceholders, iosPlaceholders)) {
errors.push(`Placeholder mismatch in ${key}`);
}
}
});
// Validate plural forms
Object.entries(strings).forEach(([key, value]) => {
if (value._type === 'plural') {
validatePluralCompleteness(key, value, errors);
}
});
return errors;
}
Length Validation: UI copy that fits on iOS might overflow on Android due to different text rendering. Include length checks in your validation:
function validateStringLengths(strings, limits) {
const warnings = [];
Object.entries(strings).forEach(([key, value]) => {
const limit = limits[key];
if (limit && value.length > limit.max) {
warnings.push(`${key} exceeds ${limit.max} characters: ${value.length}`);
}
});
return warnings;
}
This builds on the validation concepts from our localization edge cases guide, extending them to cross-platform scenarios.
Keeping Translation Teams Productive
Your cross-platform setup shouldn't make life harder for translators. They should work with clean, organized files that clearly indicate context and platform requirements.
Translator-Friendly Source Format:
{
"_metadata": {
"project": "MyApp",
"version": "2.1.0",
"platforms": ["ios", "android", "react_native"]
},
"onboarding": {
"_section_notes": "First-time user experience screens",
"welcome_title": {
"value": "Welcome to MyApp",
"_context": "Large heading on welcome screen",
"_max_length": 30
},
"get_started_button": {
"value": "Get Started",
"_context": "Primary action button",
"_platforms": {
"ios": "Continue",
"_ios_note": "Matches iOS Human Interface Guidelines"
}
}
}
}
Translation Package Generation:
// For translators, generate clean packages without technical metadata
function generateTranslatorPackage(strings, targetLanguage) {
const cleanStrings = {};
Object.entries(strings).forEach(([key, value]) => {
if (value._context) {
cleanStrings[key] = {
text: value.value || value,
context: value._context,
max_length: value._max_length
};
}
});
return cleanStrings;
}
When to Consider Professional Translation Management
As your cross-platform setup grows complex, you'll hit a point where managing everything in-house becomes costly. Signs it's time to evaluate external solutions:
- Your build process spends more time on string transformation than actual building
- Platform-specific edge cases are consuming significant engineering time
- Translation quality is suffering due to lack of context or coordination
- You're spending more on internal tooling than commercial solutions would cost
The transition point typically happens around 10-15 engineers, as covered in our analysis of when localization becomes painful at scale. The key is recognizing this inflection point before it becomes a crisis.
For teams considering this transition, our guide on when teams decide to stop managing localization in-house covers the decision-making process and what to look for in external solutions.
Making It Work Long-Term
Cross-platform string management isn't a "set it and forget it" system. It requires ongoing attention to prevent drift and maintain quality as your apps evolve.
Regular Audits: Schedule quarterly reviews of your translation files. Look for platform-specific strings that have diverged unnecessarily, unused keys that can be cleaned up, and opportunities to standardize copy across platforms.
Team Training: New developers need to understand the system. Document your string management process and include it in onboarding. Make it clear when to use platform-specific overrides versus when to push back on design requirements.
Tooling Evolution: Your string management needs will change as your apps grow. Plan to evolve your tooling gradually rather than trying to build the perfect system upfront.
The goal isn't perfect synchronization—it's controlled divergence. You want the flexibility to optimize copy for each platform while maintaining consistency where it matters and keeping your translation workflow manageable.
Cross-platform string management done right feels invisible to both developers and translators. It should make shipping localized features faster, not slower. If your current setup is fighting you instead of helping you, it's time to step back and redesign with these principles in mind.