UI Localization: 7 Best Practices I Learned From Localizing 50+ Apps

I still remember the sinking feeling when I launched our app in Arabic for the first time. The entire UI was broken—buttons overlapped, text overflowed, and the navigation was completely mirrored incorrectly. We had to pull the release and spend 3 days fixing it.
That was 3 years ago. Since then, I've localized over 50 apps and learned the hard way what works and what doesn't. Here are the 7 UI localization best practices that would have saved me countless hours of rework.
Quick Summary
| Practice | Impact | Difficulty |
|---|---|---|
| Plan for text expansion | High | Easy |
| Use Unicode throughout | Critical | Easy |
| Avoid concatenation | High | Medium |
| Support RTL from day one | Critical | Medium |
| Test with real content | High | Easy |
| Handle dates/numbers correctly | High | Easy |
| Design flexible layouts | Medium | Medium |
#1: Plan for Text Expansion (30-50% Longer)
This is the #1 mistake I see developers make. English is concise. Other languages aren't.
The Problem
English: "Save" → 4 characters
German: "Speichern" → 10 characters (+150%)
French: "Enregistrer" → 11 characters (+175%)
When I first built our app, I designed buttons for English text. When we added German, half our buttons truncated text.
The Solution
Design for 50% text expansion:
/* Bad: Fixed width */
.button {
width: 80px; /* Works for English, breaks for German */
}
/* Good: Flexible width with max */
.button {
min-width: 60px;
padding: 0 16px; /* Flexible padding */
max-width: 200px; /* Prevent overflow */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
Pro tip: Use design tokens for spacing that scale with text length:
const buttonPadding = {
short: '8px 16px', // "Save", "OK"
medium: '8px 24px', // "Submit", "Cancel"
long: '8px 32px', // "Continue", "Download"
};
#2: Use Unicode Throughout Your Stack
I once spent 2 days debugging why user names with accents were showing as "???" in our database. The culprit? A MySQL table using Latin-1 encoding instead of UTF-8.
The Checklist
Database:
-- MySQL
CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- PostgreSQL
CREATE DATABASE app_db ENCODING 'UTF8';
API Headers:
// Express.js
app.use((req, res, next) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
next();
});
HTML Meta Tag:
<meta charset="UTF-8">
File Encoding: Ensure your code editor saves files as UTF-8. This is especially important for translation files.
Test Characters
Always test with these characters to ensure your system handles Unicode:
é ñ ü ö ç ← Accented characters
你好世界 ← Chinese
こんにちは ← Japanese
مرحبا ← Arabic
שלום ← Hebrew
🎉 🔥 💯 ← Emojis
#3: Never Concatenate Strings
This mistake took me the longest to unlearn. In English, "Hello " + name works. In other languages? Not so much.
The Problem
// English: "Hello John"
const greeting = "Hello " + name;
// German: "Hallo John" (works)
// Japanese: "こんにちはJohn" (awkward, should be "Johnさん、こんにちは")
// Arabic: "مرحبا John" (wrong, Arabic is RTL)
The Solution
Use interpolation with placeholders:
// Good: Use named placeholders
const messages = {
en: "Hello {name}!",
de: "Hallo {name}!",
ja: "{name}さん、こんにちは!",
ar: "!{name} مرحبا",
};
Use ICU MessageFormat for complex cases:
const messages = {
en: "{count, plural, one {# item} other {# items}}",
ru: "{count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элементов}}",
};
Russian has 4 plural forms. English has 2. Never assume plural rules are the same across languages.
#4: Support RTL (Right-to-Left) From Day One
Arabic, Hebrew, Farsi, and Urdu are read right-to-left. If you don't plan for this, your UI will break.
The Solution
Use logical properties instead of physical:
/* Bad: Physical properties */
.element {
margin-left: 16px;
padding-right: 8px;
border-left: 1px solid #ccc;
}
/* Good: Logical properties */
.element {
margin-inline-start: 16px; /* left in LTR, right in RTL */
padding-inline-end: 8px; /* right in LTR, left in RTL */
border-inline-start: 1px solid #ccc;
}
Set direction dynamically:
import { I18nManager } from 'react-native';
// Detect RTL language
const isRTL = ['ar', 'he', 'fa', 'ur'].includes(locale);
// Configure for RTL
I18nManager.allowRTL(true);
I18nManager.forceRTL(isRTL);
Test RTL early:
I use a simple trick: flip my entire UI and see what breaks. If it looks wrong, I need to fix my layout.
/* Quick RTL test */
.rtl-test {
direction: rtl;
transform: scaleX(-1);
}
#5: Test With Real Translated Content
Lorem ipsum doesn't cut it. You need real translations to find real problems.
The Testing Strategy
1. Use "pseudo-localization" during development:
// English: "Hello World"
// Pseudo: "[Ĥëļļõ Ŵõŕļð]" — Shows expanded text and special chars
Many frameworks have built-in pseudo-localization:
// i18next pseudo-localization
i18n.init({
lng: 'pseudo',
// Automatically converts text to test format
});
2. Test with the longest translations:
Find the longest string in each language and design for that:
const longestStrings = {
button: {
en: "Continue",
de: "Fortsetzen", // +2 chars
fr: "Continuer", // +1 char
es: "Continuar", // +2 chars
// German usually wins for length
}
};
3. Get native speakers to review:
I once shipped an app with "Exit" translated as "Death" in Chinese. A native speaker review would have caught this.
#6: Handle Dates, Numbers, and Currencies Correctly
Different regions format these differently. Hardcoding formats breaks user experience.
The Problem
Date: 03/04/2026
US: March 4, 2026
UK: 3 April 2026
Japan: 2026年4月3日
Number: 1,234.56
US: 1,234.56
Germany: 1.234,56
France: 1 234,56
The Solution
Use Intl API:
// Dates
const date = new Date();
const formattedDate = new Intl.DateTimeFormat('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
// German: "3. April 2026"
// Numbers
const number = 1234.56;
const formattedNumber = new Intl.NumberFormat('de-DE').format(number);
// German: "1.234,56"
// Currencies
const price = 29.99;
const formattedPrice = new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(price);
// Japanese: "¥30"
Detect user's locale automatically:
import { getLocales } from 'expo-localization';
const userLocale = getLocales()[0].languageTag; // e.g., "de-DE"
const formatPrice = (amount, currency = 'USD') => {
return new Intl.NumberFormat(userLocale, {
style: 'currency',
currency,
}).format(amount);
};
#7: Design Flexible Layouts
Fixed layouts break. Flexible layouts survive.
Layout Best Practices
1. Use flexbox and grid:
/* Flexible button group */
.button-group {
display: flex;
flex-wrap: wrap; /* Allow wrapping on narrow screens */
gap: 8px;
}
.button {
flex: 1 1 auto; /* Grow and shrink as needed */
min-width: 120px; /* Minimum usable width */
}
2. Avoid fixed heights for text containers:
/* Bad */
.card {
height: 200px; /* Text overflow hidden */
}
/* Good */
.card {
min-height: 150px;
height: auto; /* Grows with content */
}
3. Use CSS Grid for complex layouts:
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
Quick Reference: UI Localization Checklist
Before launching in any language, verify:
- All text uses Unicode (UTF-8)
- Buttons and containers handle 50% text expansion
- No string concatenation (use placeholders)
- RTL languages display correctly
- Dates and numbers use local formats
- Currencies display with correct symbols
- Forms validate input for local conventions
- Error messages are translated
- Keyboard types match expected input
- Accessibility labels are localized
The Easier Way: AutoLocalise
Following these best practices manually is time-consuming. AutoLocalise handles most of them automatically:
import { useAutoTranslate, useAutoLocalize } from '@autolocalise/react';
function ProductCard({ product }) {
const { t, formatCurrency, formatDate } = useAutoTranslate();
return (
<View>
<Text>{t(product.name)}</Text>
<Text>{formatCurrency(product.price)}</Text>
<Text>{formatDate(product.releaseDate)}</Text>
</View>
);
}
What AutoLocalise handles:
- ✅ Unicode encoding
- ✅ Plural forms (all languages)
- ✅ Date/number/currency formatting
- ✅ RTL support
- ✅ Translation updates (instant, no rebuild)
What you still need to design for:
- Text expansion (UI layout)
- Flexible layouts
FAQ
Q: How much text expansion should I plan for?
A: Plan for 30-50% expansion. German and French typically expand the most.
Q: Which languages are RTL?
A: Arabic, Hebrew, Farsi (Persian), Urdu, and Kurdish are the most common RTL languages.
Q: Do I need to translate everything?
A: Focus on user-facing text first. Technical terms (like "API" or "JSON") often stay in English.
Q: How do I test localization without knowing the language?
A: Use pseudo-localization and native speaker reviews. Never rely on machine translation for testing.
Q: Should I use machine translation or human translation?
A: Use both. Machine translation for speed and coverage, human translation for quality and accuracy.
