Localisation and Flavoring: Customizing UI Strings by Build
When 'Delivery' Means Something Else: The Reality of Market Customization
When we started scaling our logistics platform out of Onitsha, I thought localization was just about translating English into Igbo or Yoruba. I was wrong. It’s not just language; it’s about context, tone, and the specific jargon of local markets. In Lagos, a 'Dispatch Rider' is a household term. In other markets we expanded to, the local terminology for the same role changed, as did the expectations for the delivery window.
Building a single binary for multiple regions is a recipe for maintenance hell if you don't bake localization and build-flavoring into your architecture from day one. I learned this the hard way during a deployment where our 'Delivery Confirmed' toast notification confused half our users because the local dialect used a different colloquialism for 'arrival.' If the UI doesn't speak the user's language—literally and culturally—your engagement metrics drop off a cliff. Here is how I manage build-specific strings without losing my mind.
The Problem: Hardcoded Strings are Technical Debt
We started with a StringConstants file. It was a simple map of keys to values. For a while, it worked. Then came the need for white-labeling our app for a partner logistics firm. Suddenly, we weren't just changing languages; we were changing brand names, support phone numbers, and button labels based on the build flavor. If you find yourself writing if (isPartnerApp) { 'Partner Name' } else { 'Our Name' } inside your UI code, you are already drowning in technical debt.
Hardcoded logic in your widgets creates a brittle codebase. Every time we added a new region or a new flavor, I had to touch the UI logic. That’s dangerous. UI code should be about layout and state, not about conditional brand identity. The goal is to move those strings out of the widget tree entirely and push them into the build configuration and the localization bundle.
Step-by-Step: Implementing Flutter Flavors with Localization
To decouple our strings from our UI, we use a combination of Flutter’s --flavor build argument and the intl package for localization. Here is how we structure it.
- Define Flavors in Android/iOS: You need separate Gradle flavors and iOS schemes for each market. This allows you to run
flutter run --flavor marketA. - Environment Configuration: Create an
app_config.dartthat reads from the build flavor to determine which asset folder to point to. - Localization Bundles: Instead of a single
app_en.arbfile, create specific folders for each flavor inside yourl10ndirectory.
Implementation Walkthrough
First, set up your l10n.yaml to handle multiple directories, or use a wrapper class that detects the flavor at startup. My preferred approach is using a FlavorConfig class that initializes the localizations delegate based on the current build environment.
// lib/config/flavor_config.dart
enum Flavor { onitsha, lagos, partnerX }
class FlavorConfig {
final Flavor flavor;
final String appTitle;
final String localizationPath;
FlavorConfig({
required this.flavor,
required this.appTitle,
required this.localizationPath,
});
static FlavorConfig? instance;
}
In your main.dart, you initialize this before the app runs. This ensures that when your localization delegate looks for app_onitsha.arb vs app_lagos.arb, it knows exactly which resource bundle to prioritize.
Managing Complex Strings Across Markets
Localization isn't just about static text; it's about dynamic data injection. In logistics, this is critical. A string like "Your package will arrive by {time}" needs to handle pluralization and local time formatting. The intl package is robust, but it can be finicky when you mix it with flavor-specific logic.
I’ve encountered cases where the underlying engine failed to switch the locale correctly because of how we were caching the L10n delegate. If you are using Riverpod or Bloc for state management, make sure your localization delegate is provided at the top level and is refreshed whenever the flavor environment is initialized. If you don't, you might see the 'Default' English strings even after the user selects a region.
Pro-Tips for Real-World Deployment
- Don't hardcode IDs: If your strings are keys in a database, ensure those keys are consistent across flavors. If you change a key in one
.arbfile but forget the other, your app will crash or show a blank string in production. Use a script to validate that all arb files have the same keys. - Handle Fallbacks: Always define a base language (e.g.,
en_US) that covers every single key. If yourlagos_ENfile is missing a new feature key, the app should fall back to the base, not crash. - Use ARB Tools: There are VS Code extensions that let you edit ARB files in a spreadsheet-like view. Use them. Managing JSON-like structures by hand is how you introduce trailing commas that break your build pipeline.
- Localization for Debugging: In dev builds, I add a 'Debug' tag to strings that haven't been translated yet. It saves me from the embarrassment of showing 'missing_translation' to a user in the field.
Dealing with Connectivity: The 'Offline' Translation Fallback
In Onitsha, our internet isn't always perfect. We had a recurring issue where users in low-connectivity areas were loading the app, but the language delegate was trying to fetch remote assets. We solved this by bundling all localization files directly into the APK/IPA. Do not—I repeat, do not—try to fetch your localization files from a remote server at runtime. If the network drops during that fetch, your app UI will fail to render properly. Keep everything local and ship it with the binary.
Deployment Lessons: Why It Matters
When we finally decoupled our strings from our UI logic, our 'On-Time Delivery' rates improved, not because the code ran faster, but because the users understood the interface better. In the logistics game, confusion leads to abandoned tasks. If a driver sees a button labeled 'Drop Off' when they expect 'Complete Delivery', they hesitate. A three-second hesitation in a busy street in Lagos can mean the difference between a successful delivery and a missing package.
Managing your strings by build flavor is not just an architectural choice; it's a business requirement for any app that operates across diverse regions. You need to treat your UI strings as dynamic, environment-aware entities. By moving logic into the build system and isolating your string resources, you gain the ability to launch in a new city in a weekend rather than a month.
Every time you move a hardcoded string from a widget into an arb file, you are making your app more robust, more maintainable, and ultimately, more useful to your users. Stop letting 'if' statements dictate your user experience. Build your localization engine to be as scalable as your backend. In production, the most important thing isn't the cleverness of your code; it's the clarity of your communication to the user. Get the strings right, and the rest will follow. And please, for the love of all that is holy, validate those ARB files in your CI/CD pipeline. Your future self will thank you when you're not fixing a 'null' string error on a Friday night.