Theming & Localization Strategies for Multi-Brand Apps
Master the art of making one codebase look and feel like many different apps — from color systems to right-to-left layouts.
The Multi-Brand Challenge
Your single codebase needs to become Brand A's blue and professional app, Brand B's playful orange app, and Brand C's minimalist grayscale app. Oh, and they all need to work in English, Spanish, Arabic, and Japanese.
This isn't hypothetical — it's the daily reality for teams building white-label solutions. The architecture you choose determines whether adding a new brand takes weeks or hours.
Part 1: Theming Architecture
Design Token System
The foundation of any theming system is design tokens — named values that represent design decisions:
// Design Tokens
struct DesignTokens {
// Colors
let colorPrimary: Color
let colorSecondary: Color
let colorBackground: Color
let colorSurface: Color
let colorError: Color
let colorSuccess: Color
// Text Colors
let textPrimary: Color
let textSecondary: Color
let textTertiary: Color
let textOnPrimary: Color
// Typography
let fontFamilyPrimary: String
let fontFamilySecondary: String
let fontSizeHeading1: CGFloat
let fontSizeHeading2: CGFloat
let fontSizeBody: CGFloat
let fontSizeCaption: CGFloat
// Spacing
let spacingXS: CGFloat // 4
let spacingS: CGFloat // 8
let spacingM: CGFloat // 16
let spacingL: CGFloat // 24
let spacingXL: CGFloat // 32
// Borders & Corners
let borderRadiusSmall: CGFloat
let borderRadiusMedium: CGFloat
let borderRadiusLarge: CGFloat
let borderWidth: CGFloat
// Shadows
let shadowSmall: ShadowDefinition
let shadowMedium: ShadowDefinition
let shadowLarge: ShadowDefinition
}
Theme Protocol
Define a protocol that all themes must implement:
protocol Theme {
var tokens: DesignTokens { get }
var name: String { get }
var isDark: Bool { get }
// Semantic colors
var primaryButtonBackground: Color { get }
var primaryButtonText: Color { get }
var navigationBarBackground: Color { get }
var tabBarTint: Color { get }
}
extension Theme {
// Default implementations using tokens
var primaryButtonBackground: Color { tokens.colorPrimary }
var primaryButtonText: Color { tokens.textOnPrimary }
}
Theme Manager
Centralize theme management with observable state:
class ThemeManager: ObservableObject {
static let shared = ThemeManager()
@Published private(set) var currentTheme: Theme
private let brandConfig: BrandConfiguration
init(brandConfig: BrandConfiguration = .current) {
self.brandConfig = brandConfig
self.currentTheme = ThemeLoader.load(brandConfig.themeIdentifier)
}
func setDarkMode(_ enabled: Bool) {
currentTheme = enabled
? ThemeLoader.loadDark(brandConfig.themeIdentifier)
: ThemeLoader.loadLight(brandConfig.themeIdentifier)
}
}
// SwiftUI usage
struct ContentView: View {
@EnvironmentObject var themeManager: ThemeManager
var body: some View {
VStack {
Text("Hello")
.foregroundColor(themeManager.currentTheme.tokens.textPrimary)
}
.background(themeManager.currentTheme.tokens.colorBackground)
}
}
Theme File Format
Store themes as JSON for easy editing and version control:
{
"name": "Brand A Light",
"isDark": false,
"tokens": {
"colorPrimary": "#0066CC",
"colorSecondary": "#FF6600",
"colorBackground": "#FFFFFF",
"colorSurface": "#F5F5F5",
"textPrimary": "#1A1A1A",
"textSecondary": "#666666",
"fontFamilyPrimary": "Helvetica Neue",
"fontSizeBody": 16,
"spacingM": 16,
"borderRadiusMedium": 8
}
}
Dynamic Theme Switching
Support runtime theme changes for dark mode and user preferences:
extension View {
func themed() -> some View {
self.modifier(ThemedViewModifier())
}
}
struct ThemedViewModifier: ViewModifier {
@EnvironmentObject var themeManager: ThemeManager
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
content
.onChange(of: colorScheme) { newScheme in
themeManager.setDarkMode(newScheme == .dark)
}
}
}
Part 2: Localization Architecture
String Externalization
Never hardcode strings. Ever.
// Bad
Text("Welcome back!")
// Good
Text(L10n.welcomeBack)
// Using SwiftGen or similar
enum L10n {
static let welcomeBack = NSLocalizedString(
"welcome_back",
comment: "Welcome message on home screen"
)
}
Structured Localization Files
Organize strings by feature:
Localization/
en.lproj/
Authentication.strings
Dashboard.strings
Payments.strings
Common.strings
es.lproj/
ar.lproj/
ja.lproj/
Brand + Locale Matrix
Some strings differ by brand AND locale:
struct LocalizationManager {
let brand: Brand
let locale: Locale
func string(for key: String) -> String {
// Priority: Brand+Locale → Brand+Default → Base+Locale → Base
if let brandLocalized = bundle(for: brand, locale: locale)?
.localizedString(forKey: key, value: nil, table: nil),
brandLocalized != key {
return brandLocalized
}
return Bundle.main.localizedString(forKey: key, value: key, table: nil)
}
}
Right-to-Left (RTL) Support
RTL languages (Arabic, Hebrew, Urdu) require layout mirroring:
// Use leading/trailing, not left/right
HStack {
Image(systemName: "arrow.left")
Text("Back")
}
.environment(\.layoutDirection, locale.isRTL ? .rightToLeft : .leftToRight)
// Flip directional icons
Image(systemName: "arrow.left")
.flipsForRightToLeftLayoutDirection(true)
Date, Number, and Currency Formatting
Always use formatters — never string concatenation:
// Bad
let price = "$\(amount)"
// Good
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale.current
let price = formatter.string(from: NSNumber(value: amount))
// Modern SwiftUI
Text(amount, format: .currency(code: "USD"))
Pluralization
Handle plural rules correctly:
/* Localizable.stringsdict */
<dict>
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No items</string>
<key>one</key>
<string>%d item</string>
<key>other</key>
<string>%d items</string>
</dict>
</dict>
</dict>
Part 3: Asset Management
Brand-Specific Assets
Structure assets by brand with fallback to base:
Assets/
Base.xcassets/
logo.imageset
icons/
backgrounds/
BrandA.xcassets/
logo.imageset (override)
brand-specific.imageset
BrandB.xcassets/
logo.imageset (override)
Asset Loading
Load brand assets with fallback:
struct BrandAssets {
let brand: Brand
func image(named name: String) -> UIImage? {
// Try brand-specific first
if let brandImage = UIImage(named: name, in: brand.bundle, with: nil) {
return brandImage
}
// Fall back to base
return UIImage(named: name)
}
}
Icon Customization
Allow brands to customize common icons:
{
"icons": {
"home": "house.fill",
"profile": "person.fill",
"settings": "gearshape.fill",
"notifications": "custom_brand_bell"
}
}
Part 4: Testing Strategies
Visual Regression Testing
Automate screenshot comparison across brands and locales:
class VisualRegressionTests: XCTestCase {
func testLoginScreenAllBrands() {
for brand in Brand.allCases {
for locale in Locale.supported {
let app = configureApp(brand: brand, locale: locale)
app.launch()
let screenshot = app.screenshot()
verify(screenshot, named: "login_\(brand)_\(locale)")
}
}
}
}
Pseudo-Localization
Test layout robustness with pseudo-locales:
// Pseudo-locale that doubles string length
"Welcome" → "[Wééélcóóómééé]"
// Reveals truncation issues before real translation
RTL Testing Without Translation
Force RTL layout to test without Arabic strings:
// In scheme settings
-AppleTextDirection YES
-NSForceRightToLeftWritingDirection YES
Implementation Checklist
Theming
- Design token system defined
- Theme protocol with all semantic values
- ThemeManager with observable state
- Dark mode support
- Theme file format for easy editing
- Compile-time theme validation
Localization
- All strings externalized
- String files organized by feature
- RTL layout support
- Proper formatters for dates/numbers/currency
- Pluralization rules
- Brand + locale matrix handling
Assets
- Asset catalogs per brand
- Fallback asset loading
- Configurable icons
- Image optimization for different screens
Testing
- Visual regression tests per brand
- Pseudo-localization testing
- RTL layout testing
- Locale edge case testing
Conclusion
Theming and localization seem like surface-level concerns, but they touch every part of your application. Getting the architecture right means:
- Adding a new brand is hours, not weeks
- Adding a new language is configuration, not code
- Design changes propagate automatically
- Testing is systematic and automated
Invest in the foundation early. Your future self will thank you.
Strategies refined through multi-brand, multi-locale mobile deployments serving global audiences.
