Back to all articles

Mobile App Localization & Internationalization: Complete Guide 2025

Localized apps see 128% more downloads and 26% higher revenue per user in international markets. Yet 75% of apps support only English. This guide shows you how to internationalize your codebase and localize content for global success.

Why Localization Matters

Business Impact

  • More downloads: 128% increase in non-English markets
  • Higher revenue: 26% more revenue per user
  • Better engagement: 2.5x higher retention
  • Market expansion: Access 6+ billion non-English speakers
  • Competitive advantage: Most apps don't localize
  • App Store featuring: Localized apps get preferential treatment

Internationalization vs Localization

Internationalization (i18n):
- Preparing code to support multiple languages
- Extracting strings from code
- Handling different formats
- Supporting RTL layouts
- One-time development effort

Localization (l10n):
- Translating content
- Adapting culturally
- Testing in each language
- Ongoing process per language

Process:
1. Internationalize codebase (i18n)
2. Localize content (l10n)
3. Test and refine
4. Maintain translations

iOS Internationalization

Setup

// 1. Add languages in Xcode
// Project → Info → Localizations → + Button
// Add: Spanish, French, German, Japanese, etc.

// 2. Create Localizable.strings
// New File → Strings File → "Localizable.strings"
// Localize it (File Inspector → Localize)

// 3. Base language (en)
// Localizable.strings (English)
"welcome_message" = "Welcome to our app!";
"login_button" = "Log In";
"logout_button" = "Log Out";
"settings_title" = "Settings";
"error_network" = "Network error. Please try again.";

// With parameters
"items_count" = "%d items";
"greeting" = "Hello, %@!";

// 4. Spanish translation
// Localizable.strings (Spanish)
"welcome_message" = "¡Bienvenido a nuestra aplicación!";
"login_button" = "Iniciar sesión";
"logout_button" = "Cerrar sesión";
"settings_title" = "Configuración";
"error_network" = "Error de red. Por favor, inténtelo de nuevo.";

"items_count" = "%d artículos";
"greeting" = "¡Hola, %@!";

Using Localized Strings

import UIKit

// Basic usage
let message = NSLocalizedString("welcome_message", comment: "Welcome screen message")
label.text = message

// With parameters
let count = 5
let text = String(format: NSLocalizedString("items_count", comment: ""), count)
// Result: "5 items" (English) or "5 artículos" (Spanish)

let name = "John"
let greeting = String(format: NSLocalizedString("greeting", comment: ""), name)
// Result: "Hello, John!" or "¡Hola, John!"

// Pluralization
let format = String.localizedStringWithFormat(
    NSLocalizedString("items_count", comment: ""),
    count
)

// Custom strings file
func localizedString(_ key: String, table: String = "Localizable") -> String {
    NSLocalizedString(key, tableName: table, comment: "")
}

// Usage
let errorMsg = localizedString("error_network", table: "Errors")

Plural Rules

// Localizable.stringsdict




    items_count
    
        NSStringLocalizedFormatKey
        %#@items@
        items
        
            NSStringFormatSpecTypeKey
            NSStringPluralRuleType
            NSStringFormatValueTypeKey
            d
            zero
            No items
            one
            1 item
            other
            %d items
        
    



// Usage
let count = 0
let text = String.localizedStringWithFormat(
    NSLocalizedString("items_count", comment: ""),
    count
)
// count = 0: "No items"
// count = 1: "1 item"
// count = 5: "5 items"

Date and Number Formatting

import Foundation

// Date formatting
let date = Date()
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = Locale.current

let dateString = formatter.string(from: date)
// English: "Dec 31, 2025 at 3:45 PM"
// Spanish: "31 dic 2025, 15:45"
// Japanese: "2025/12/31 15:45"

// Relative date
let relativeFormatter = RelativeDateTimeFormatter()
relativeFormatter.unitsStyle = .full
let relativeString = relativeFormatter.localizedString(for: date, relativeTo: Date())
// "in 2 hours" or "hace 2 horas"

// Number formatting
let number = 1234.56
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let formattedNumber = numberFormatter.string(from: NSNumber(value: number))
// English: "1,234.56"
// German: "1.234,56"
// French: "1 234,56"

// Currency formatting
let price = 99.99
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.locale = Locale.current
let priceString = currencyFormatter.string(from: NSNumber(value: price))
// US: "$99.99"
// UK: "£99.99"
// Japan: "¥100"
// Euro: "99,99 €"

// Percentage
let percent = 0.85
let percentFormatter = NumberFormatter()
percentFormatter.numberStyle = .percent
let percentString = percentFormatter.string(from: NSNumber(value: percent))
// "85%"

RTL Support

import UIKit

// Auto-Layout handles RTL automatically if done correctly

// Use leading/trailing instead of left/right
// ✓ Good
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)

// ✗ Bad
label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16)
label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16)

// Text alignment
label.textAlignment = .natural // Auto-adjusts for RTL

// Image flipping
imageView.image = UIImage(named: "arrow")?.imageFlippedForRightToLeftLayoutDirection()

// Check current direction
let isRTL = UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft

// Force specific direction
view.semanticContentAttribute = .forceLeftToRight // Always LTR
view.semanticContentAttribute = .forceRightToLeft // Always RTL

// Test RTL in simulator
// Scheme → Options → App Language → Arabic/Hebrew

Android Internationalization

Setup



    My App
    Welcome to our app!
    Log In
    Log Out
    Settings
    Network error. Please try again.

    
    %d items
    Hello, %s!




    Mi aplicación
    ¡Bienvenido a nuestra aplicación!
    Iniciar sesión
    Cerrar sesión
    Configuración
    Error de red. Por favor, inténtelo de nuevo.

    %d artículos
    ¡Hola, %s!





Using Localized Strings

import android.content.Context

// In Activity/Fragment
val message = getString(R.string.welcome_message)
textView.text = message

// With parameters
val count = 5
val text = getString(R.string.items_count, count)
// Result: "5 items"

val name = "John"
val greeting = getString(R.string.greeting, name)
// Result: "Hello, John!"

// From Context
fun getLocalizedString(context: Context, resId: Int, vararg args: Any): String {
    return context.getString(resId, *args)
}

Plural Rules



    
        No items
        1 item
        %d items
    




    
        %d предмет
        %d предмета
        %d предметов
        %d предметов
    


// Usage
val count = 5
val text = resources.getQuantityString(R.plurals.items_count, count, count)
// English: "5 items"
// Russian: "5 предметов"

Date and Number Formatting

import java.text.DateFormat
import java.text.NumberFormat
import java.util.*

// Date formatting
val date = Date()
val dateFormat = DateFormat.getDateTimeInstance(
    DateFormat.MEDIUM,
    DateFormat.SHORT,
    Locale.getDefault()
)
val dateString = dateFormat.format(date)
// English: "Dec 31, 2025, 3:45 PM"
// Spanish: "31 dic 2025, 15:45"

// Relative date
val now = System.currentTimeMillis()
val yesterday = now - 24 * 60 * 60 * 1000
val relativeDate = android.text.format.DateUtils.getRelativeTimeSpanString(
    yesterday,
    now,
    android.text.format.DateUtils.DAY_IN_MILLIS
)
// "Yesterday" or "Ayer"

// Number formatting
val number = 1234.56
val numberFormat = NumberFormat.getNumberInstance(Locale.getDefault())
val formattedNumber = numberFormat.format(number)
// English: "1,234.56"
// German: "1.234,56"

// Currency formatting
val price = 99.99
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault())
val priceString = currencyFormat.format(price)
// US: "$99.99"
// UK: "£99.99"
// Euro: "99,99 €"

// Percentage
val percent = 0.85
val percentFormat = NumberFormat.getPercentInstance(Locale.getDefault())
val percentString = percentFormat.format(percent)
// "85%"

RTL Support















// Check RTL in code
val isRTL = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL

// Force specific direction
view.layoutDirection = View.LAYOUT_DIRECTION_LTR // Always LTR
view.layoutDirection = View.LAYOUT_DIRECTION_RTL // Always RTL

// Test RTL
// Settings → Developer Options → Force RTL layout direction

Translation Management

Translation Workflow

1. Extract strings from code
   - iOS: Export for localization (Xcode)
   - Android: Use strings.xml

2. Prepare for translation
   - Context comments
   - Character limits
   - Screenshots
   - Glossary

3. Send to translators
   - Professional translation services
   - Community translation (Crowdin, Lokalise)
   - Machine translation (initial draft only)

4. Review translations
   - Native speaker review
   - Context verification
   - Cultural appropriateness

5. Import translations
   - iOS: Import localizations
   - Android: Add strings.xml files

6. Test in each language
   - UI layout (text overflow)
   - RTL layouts
   - Date/number formats
   - Cultural appropriateness

7. Continuous updates
   - Track new strings
   - Update existing translations
   - Version management

Translation Services

Professional Services:
- Gengo: $0.06-0.10 per word
- One Hour Translation: Fast turnaround
- SDL: Enterprise solutions

Platforms:
- Crowdin: Collaborative translation
- Lokalise: Developer-focused
- Phrase: Translation management
- POEditor: Simple interface

DIY Options:
- Community volunteers (open source)
- Google Translate (draft only, not production)
- DeepL (better than Google, still review needed)

Best practice:
Professional translation for:
✓ Marketing copy
✓ App Store listings
✓ Critical UI text

Community/machine for:
✓ Initial drafts
✓ Less critical content
✓ Quick prototypes

Cultural Adaptation

Beyond Translation

Consider cultural differences:

Colors:
- Red: Danger (Western) vs Good luck (China)
- White: Purity (Western) vs Death (Asia)
- Yellow: Caution (Western) vs Sacred (Buddhism)

Symbols:
- Thumbs up: OK (Western) vs Offensive (Middle East)
- OK sign: Good (US) vs Rude (Brazil)
- Left hand: Normal (Western) vs Unclean (Middle East)

Images:
- Showing skin: Normal (Western) vs Conservative (Middle East)
- Religious symbols: Context-dependent
- Hand gestures: Vary by culture

Text:
- Formal vs informal: Varies by language
- Idioms don't translate: Use simple language
- Humor: Often culture-specific

Examples to adapt:

US → Japan:
- Direct → Indirect communication
- Aggressive CTAs → Polite suggestions
- Individual focus → Group harmony

US → Middle East:
- Left-to-right → Right-to-left
- Casual imagery → Conservative
- Weekend = Sat-Sun → Thu-Fri

US → Germany:
- Informal → Formal tone
- Marketing speak → Technical accuracy
- Subjective claims → Objective facts

Testing Checklist

For each language:

Layout:
□ Text fits without truncation
□ Buttons sized correctly
□ Line breaks appropriate
□ Images not cut off
□ Spacing looks good
□ RTL layout correct (if applicable)

Functionality:
□ All features work
□ Forms submit correctly
□ Search works with language
□ Sorting works correctly
□ Date/time displays correctly

Content:
□ Translation accurate
□ Tone appropriate
□ No offensive content
□ Currency correct
□ Units correct (metric/imperial)

Cultural:
□ Images appropriate
□ Colors appropriate
□ Symbols appropriate
□ Examples relevant

App Store Localization

Localized Metadata

For each language, localize:

✓ App name (if appropriate)
✓ Subtitle/Short description
✓ Description
✓ Keywords (iOS)
✓ What's new (release notes)
✓ Screenshots
✓ Preview video (if applicable)
✓ Support URL
✓ Privacy policy URL

iOS App Store:
- 30 chars: App name
- 30 chars: Subtitle
- 100 chars: Keywords
- 4000 chars: Description
- 4000 chars: What's new

Google Play:
- 30 chars: Title
- 80 chars: Short description
- 4000 chars: Full description
- 500 chars: What's new

Screenshot guidelines:
- Show app in target language
- Use relevant examples for culture
- 5-8 screenshots per language
- Text overlays in target language

ROI of localized metadata:
- 767% increase in downloads (study)
- Higher conversion rate
- Better search ranking in local stores

Maintenance

Keeping Translations Updated

Workflow for updates:

1. Track new strings
   - Mark untranslated strings
   - Version strings
   - Track changes

2. Batch translations
   - Accumulate changes
   - Send batch to translators
   - More cost-effective

3. Versioning
   - Tag strings by app version
   - Know what needs translation
   - Rollback if needed

4. Automated checks
   - Missing translations
   - Unused strings
   - Format string mismatches

5. Continuous improvement
   - User feedback
   - In-context translation issues
   - A/B test translations

Conclusion

Localization unlocks global markets and dramatically increases your app's reach and revenue. While internationalization requires upfront technical work, proper implementation makes adding new languages straightforward. Start with i18n best practices from day one, prioritize key markets, use professional translation for critical content, and continuously improve based on user feedback. The investment pays off through increased downloads, better engagement, and higher revenue across international markets.

Localized apps need localized support. Our support URL generator creates support pages in multiple languages, ensuring your international users get the same great support experience as your domestic audience.

Need a Support URL for Your App?

Generate a compliant, professional support page in under a minute. Our easy-to-use generator creates everything you need for App Store and Google Play submissions.