Every missed translation, truncated button, or hardcoded date format that ships to production erodes user trust and inflates support tickets. For SaaS product teams running two-week sprints, localization often feels like a tax on velocity, something bolted on after feature freeze, if it's addressed at all. This playbook changes that. It walks product managers and engineers through the foundations of internationalization, string management, release workflows, mobile-specific considerations, and automated quality gates you can wire directly into CI. The goal is straightforward: ship every locale with the same confidence you ship your default language, without slowing your release train. Whether you're localizing into five languages or fifty, these practices keep quality high and cycle times short.
If you need an execution partner to handle the translation layer so your engineers can focus on building, explore how Ollang fits into your pipeline.
Internationalization Foundations
Before a single string is translated, your codebase needs to be internationalization-ready. Retrofitting i18n into a mature product is dramatically more expensive than building it in from the start, so treat these foundations as non-negotiable infrastructure.
Resource File Formats and Structure
Every UI string should live in an externalized resource file, never inline in source code. The format you choose depends on your stack:
| Platform / Framework | Common Format | Notes |
|---|---|---|
| React / Next.js | JSON (flat or nested) | Pairs well with react-intl or i18next |
| Angular | XLIFF or JSON | Angular CLI extracts to XLIFF by default |
| iOS (Swift/ObjC) | .strings / .stringsdict | Xcode manages .lproj bundles |
| Android (Kotlin/Java) | XML (strings.xml) | Stored in res/values-<locale>/ |
| Flutter | ARB (Application Resource Bundle) | JSON-based, supports ICU syntax |
| Backend / API | YAML, PO, JSON | Depends on framework (Rails, Django, etc.) |
Keep one canonical source of truth per platform. Avoid splitting strings across multiple files unless you have a clear module-based architecture that justifies it. Name files consistently, messages.en.json, messages.de.json, so tooling can discover them automatically.
ICU Message Syntax for Plurals and Gender
Hardcoding plural logic with conditionals like count === 1 ? "item" : "items" breaks the moment you add a language with complex plural rules. Russian has four plural forms; Arabic has six. The ICU MessageFormat specification handles this cleanly:
{count, plural,
one {You have # item in your cart.}
other {You have # items in your cart.}
}
Translators can then supply the correct number of plural branches for their language without touching code. The same syntax supports select for gender and selectordinal for ordinal numbers. Adopt ICU early, migrating from naive string concatenation to ICU across hundreds of keys is painful.
Date, Number, and Currency Formatting
Never format dates or numbers manually. Use the Intl API in JavaScript, DateFormatter in Swift, or java.text.NumberFormat in Kotlin/Java. These APIs respect the user's locale automatically:
- Dates: MM/DD/YYYY is meaningless in most of Europe. Use locale-aware formatters and let the runtime decide.
- Numbers: The decimal separator is a comma in Germany and a period in the US.
- Currency: Always pair the amount with its currency code. Displaying $100 to a Canadian user is ambiguous, is it CAD or USD?
Store dates in UTC and numbers as raw values. Format at the presentation layer, never in the database or API response.
RTL Layout, Bidirectional Text, and Font Coverage
Supporting right-to-left languages like Arabic, Hebrew, and Farsi requires more than flipping a CSS direction. Your layout needs logical properties, margin-inline-start instead of margin-left, padding-inline-end instead of padding-right. Modern CSS logical properties, documented in the MDN web docs on CSS logical properties, make this manageable.
Watch for bidirectional (bidi) text issues where LTR content (like URLs, brand names, or numbers) appears inside RTL sentences. Use Unicode bidi control characters or the dir="auto" attribute to handle mixed-direction runs.
Font coverage is equally important. Your brand typeface may not include glyphs for CJK (Chinese, Japanese, Korean) scripts or Devanagari. Define a font stack that gracefully falls back to system fonts with full Unicode coverage for each target script.
String Key Design and Translator Context
Good string keys and rich context are the difference between a translation that reads naturally and one that confuses users.
Naming Conventions That Scale
String keys should be descriptive, hierarchical, and stable. A key like str_47 tells a translator nothing. A key like dashboard.widget.revenue.title communicates where the string appears and what role it plays.
Adopt a consistent convention:
- <feature>.<component>.<element> for UI strings
- <feature>.error.<errorType> for error messages
- <feature>.tooltip.<element> for contextual help
Avoid embedding the English text in the key (button_submit_your_order) because the English copy will change while the key should remain stable. Stable keys protect translation memory matches and prevent unnecessary re-translation costs.
Adding Developer Notes and Screenshots
Translators work in a TMS (translation management system), not in your app. Without context, they're guessing. Every string should carry:
- A developer note explaining where the string appears, what it refers to, and any constraints (character limit, variable placeholders).
- A screenshot showing the string in its actual UI context.
Automate screenshot capture as part of your build process. Tools like Storybook, Playwright, or Xcode's UI testing framework can render every screen state and attach images to the corresponding string keys. This single investment dramatically reduces translation errors and back-and-forth with linguists.
Handling Variables, Placeholders, and Concatenation Pitfalls
String concatenation is the enemy of good localization. A pattern like "Welcome, " + userName + "! You have " + count + " notifications." forces a specific word order that doesn't hold across languages. In Japanese, the verb comes last. In German, the sentence structure may require the count in a different position.
Use named placeholders instead: Welcome, {userName}! You have {notificationCount} notifications. This lets translators reorder elements freely. Document each placeholder's type and expected value range in the developer note so translators know whether {notificationCount} could be zero, one, or thousands.
Release Workflows: String Freeze vs. Continuous Localization
Why String Freeze Slows You Down
The traditional approach, freeze all strings a week before release, send them for translation, wait, integrate, made sense when software shipped quarterly. In a SaaS world with weekly or daily deployments, string freeze creates a bottleneck. Features sit idle waiting for translations. Hotfixes that touch UI copy require an emergency translation cycle. And the freeze window itself becomes a negotiation between product, engineering, and localization teams.
String freeze also encourages batching, which means translators receive large volumes at once, leading to inconsistency and rushed work.
Branch-Based Continuous Localization
Continuous localization eliminates the freeze by integrating translation into your development workflow in near-real-time. Here's how it works:
1. A developer adds or modifies a string key on a feature branch.
2. A CI hook pushes the new or changed key to the TMS automatically.
3. Translators work on the string while the feature is still in development.
4. Translated strings are pulled back into the branch (or merged into main) before the feature ships.
This mirrors how continuous integration works for code: small, frequent changes instead of large, risky batches. The key enabler is a bidirectional sync between your repository and TMS, Ollang and most modern platforms (Phrase, Lokalise, Crowdin) support GitHub/GitLab integrations natively.
Feature Flags for Locale-Specific Rollouts
Not every locale needs to launch simultaneously. Feature flags let you gate a feature by locale, so you can ship to English users on day one and roll out to Japanese and German users once translations pass QA.
This approach also enables localized experiments. You can A/B test localized onboarding flows in specific markets without waiting for full multilingual coverage. If a feature's translations aren't ready, the flag keeps users on the previous experience rather than exposing them to untranslated strings.
If you’re ready to move beyond freezes and wire translations into your branch strategy, set up a continuous localization workflow with Ollang.
Mobile Localization Specifics
iOS and Android Resource Conventions
Mobile platforms have their own resource systems, and respecting their conventions avoids unnecessary friction.
On iOS, localizable strings live in .lproj directories (en.lproj/Localizable.strings, ja.lproj/Localizable.strings). Use .stringsdict files for plurals, they support the full CLDR plural categories. SwiftUI's LocalizedStringKey makes string extraction straightforward, but watch for strings constructed dynamically that the extraction tooling misses.
On Android, strings go in res/values-<locale>/strings.xml. Android's plurals resource type handles plural forms, and the translatable="false" attribute prevents non-translatable strings (like API keys or format patterns) from cluttering the translation queue.
Both platforms support string arrays and parameterized strings, but the syntax differs. If you share a backend with your web app, consider a shared source format (like JSON or XLIFF) that gets compiled into platform-native formats during the build.
Over-the-Air Translation Updates
App store review cycles make it impractical to push a new binary every time a translation is corrected. Over-the-air (OTA) update mechanisms let you push updated string bundles to users without a full app release.
OTA is especially valuable for:
- Fixing translation bugs reported by users in production.
- Updating marketing copy for seasonal campaigns.
- Rolling out translations for features that were gated by locale flags.
Ensure your OTA mechanism includes versioning and fallback logic, if a downloaded bundle is corrupted or incomplete, the app should gracefully fall back to the bundled strings.
