Ultimate I18n Guide for Android, React, and Vue: JSON, XML, and Translation Best Practices
Internationalization (i18n) is essential for reaching a global audience with your app. Whether you're building for Android, React, or Vue, this comprehensive guide covers everything you need to know about implementing i18n correctly—from file formats to best practices and common pitfalls.
Table of Contents
- What is i18n and Why It Matters
- Translation File Formats: JSON vs XML
- Android i18n Implementation
- React i18n with i18next
- Vue i18n Implementation
- Cross-Platform Best Practices
- Common Pitfalls and How to Avoid Them
- Translation Management Workflow
- Library Comparison
What is i18n and Why It Matters {#what-is-i18n}
i18n (internationalization) is the process of designing your application so it can be adapted to various languages and regions without engineering changes. The term "i18n" comes from the 18 letters between "i" and "n" in "internationalization."
Key Benefits:
- Larger market reach: Access 75% of internet users who don't speak English
- Better user experience: Users prefer apps in their native language
- Increased revenue: Localized apps see 26% higher conversion rates
- SEO advantages: Rank for local language search queries
i18n vs L10n:
- i18n (Internationalization): Preparing your code for multiple languages
- L10n (Localization): Actually translating content for specific locales
Translation File Formats: JSON vs XML {#file-formats}
JSON Format
JSON is the preferred format for web applications (React, Vue) and increasingly used in mobile apps.
Pros:
- Lightweight and easy to parse
- Native support in JavaScript
- Nested structures for organization
- Wide tooling support
Example structure:
{
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"loading": "Loading..."
},
"auth": {
"login": "Log In",
"logout": "Log Out",
"register": "Create Account",
"forgotPassword": "Forgot Password?"
},
"errors": {
"network": "Network error. Please try again.",
"required": "This field is required",
"invalidEmail": "Please enter a valid email"
}
}
XML Format
XML is the native format for Android and provides more structure for complex translations.
Pros:
- Native Android support
- Built-in pluralization
- Comment support for translators
- Strong typing with attributes
Example structure:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<!-- Authentication -->
<string name="login">Log In</string>
<string name="logout">Log Out</string>
<!-- Plurals -->
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>
When to Use Which Format
| Use Case | Recommended Format |
|---|---|
| Android Native | XML (strings.xml) |
| React/Vue Web | JSON |
| React Native | JSON |
| Cross-platform (shared) | JSON |
| Complex pluralization | XML or ICU format |
Android i18n Implementation {#android-i18n}
Project Structure
app/src/main/res/
├── values/
│ └── strings.xml # Default (English)
├── values-de/
│ └── strings.xml # German
├── values-es/
│ └── strings.xml # Spanish
├── values-fr/
│ └── strings.xml # French
├── values-zh-rCN/
│ └── strings.xml # Chinese (Simplified)
└── values-ar/
└── strings.xml # Arabic (RTL)
Basic strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">My App</string>
<string name="welcome_title">Welcome</string>
<string name="welcome_message">Thanks for using our app!</string>
<!-- With parameters -->
<string name="greeting">Hello, %1$s!</string>
<string name="items_selected">%1$d of %2$d selected</string>
<!-- With HTML -->
<string name="terms_link"><![CDATA[
I agree to the <a href="https://example.com/terms">Terms of Service</a>
]]></string>
</resources>
Using Strings in Kotlin
// In Activity/Fragment
val welcome = getString(R.string.welcome_title)
val greeting = getString(R.string.greeting, userName)
// Plurals
val itemsText = resources.getQuantityString(
R.plurals.items_count,
count,
count
)
// In Jetpack Compose
@Composable
fun WelcomeScreen() {
Text(text = stringResource(R.string.welcome_title))
Text(text = stringResource(R.string.greeting, userName))
}
Android JSON Alternative
For dynamic translations or server-driven content:
class TranslationManager(private val context: Context) {
private var translations: Map<String, Any> = emptyMap()
suspend fun loadTranslations(locale: String) {
// Load from assets or API
val json = context.assets.open("i18n/$locale.json")
.bufferedReader().use { it.readText() }
translations = Json.decodeFromString(json)
}
fun t(key: String, vararg args: Any): String {
val keys = key.split(".")
var value: Any? = translations
for (k in keys) {
value = (value as? Map<*, *>)?.get(k)
}
return (value as? String)?.let {
if (args.isNotEmpty()) String.format(it, *args) else it
} ?: key
}
}
// Usage
val tm = TranslationManager(context)
tm.loadTranslations("de")
val text = tm.t("auth.login") // "Anmelden"
React i18n with i18next {#react-i18n}
Installation
npm install i18next react-i18next i18next-browser-languagedetector
Project Structure
src/
├── i18n/
│ ├── index.ts
│ └── locales/
│ ├── en/
│ │ ├── common.json
│ │ └── auth.json
│ ├── de/
│ │ ├── common.json
│ │ └── auth.json
│ └── es/
│ ├── common.json
│ └── auth.json
└── App.tsx
i18n Configuration
// src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Import translations
import enCommon from './locales/en/common.json';
import enAuth from './locales/en/auth.json';
import deCommon from './locales/de/common.json';
import deAuth from './locales/de/auth.json';
const resources = {
en: {
common: enCommon,
auth: enAuth,
},
de: {
common: deCommon,
auth: deAuth,
},
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
defaultNS: 'common',
interpolation: {
escapeValue: false, // React already escapes
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
},
});
export default i18n;
Translation Files
// locales/en/common.json
{
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"loading": "Loading...",
"greeting": "Hello, {{name}}!",
"itemsCount_one": "{{count}} item",
"itemsCount_other": "{{count}} items"
}
// locales/de/common.json
{
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"loading": "Wird geladen...",
"greeting": "Hallo, {{name}}!",
"itemsCount_one": "{{count}} Artikel",
"itemsCount_other": "{{count}} Artikel"
}
Using Translations in Components
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('greeting', { name: 'Max' })}</h1>
<p>{t('itemsCount', { count: 5 })}</p>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('de')}>Deutsch</button>
</div>
);
}
i18next Placeholder Syntax
i18next uses double curly braces for interpolation:
{
"welcome": "Welcome, {{username}}!",
"orderStatus": "Order #{{orderId}} is {{status}}",
"price": "Price: {{amount, currency(USD)}}",
"date": "Date: {{date, datetime}}"
}
t('welcome', { username: 'John' })
t('orderStatus', { orderId: 123, status: 'shipped' })
React i18next Pluralization
i18next handles pluralization with suffixes:
{
"message_zero": "No messages",
"message_one": "One message",
"message_other": "{{count}} messages"
}
t('message', { count: 0 }) // "No messages"
t('message', { count: 1 }) // "One message"
t('message', { count: 5 }) // "5 messages"
Vue i18n Implementation {#vue-i18n}
Installation
npm install vue-i18n@9
Project Structure
src/
├── i18n/
│ ├── index.ts
│ └── locales/
│ ├── en.json
│ ├── de.json
│ └── es.json
├── main.ts
└── App.vue
Vue i18n Configuration
// src/i18n/index.ts
import { createI18n } from 'vue-i18n';
import en from './locales/en.json';
import de from './locales/de.json';
const i18n = createI18n({
legacy: false, // Use Composition API
locale: 'en',
fallbackLocale: 'en',
messages: {
en,
de,
},
});
export default i18n;
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import i18n from './i18n';
const app = createApp(App);
app.use(i18n);
app.mount('#app');
Translation Files
// locales/en.json
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"auth": {
"login": "Log In",
"logout": "Log Out"
},
"greeting": "Hello, {name}!",
"items": "no items | one item | {count} items"
}
// locales/de.json
{
"nav": {
"home": "Startseite",
"about": "Über uns",
"contact": "Kontakt"
},
"auth": {
"login": "Anmelden",
"logout": "Abmelden"
},
"greeting": "Hallo, {name}!",
"items": "keine Artikel | ein Artikel | {count} Artikel"
}
Vue i18n in Templates
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
function changeLanguage(lang: string) {
locale.value = lang;
}
</script>
<template>
<nav>
<a href="/">{{ t('nav.home') }}</a>
<a href="/about">{{ t('nav.about') }}</a>
</nav>
<h1>{{ t('greeting', { name: 'Max' }) }}</h1>
<p>{{ t('items', 5) }}</p>
<select v-model="locale">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</template>
Vue i18n Translate in JS
For translations outside templates:
// In Composition API
import { useI18n } from 'vue-i18n';
export function useMyComposable() {
const { t } = useI18n();
function showNotification() {
alert(t('notifications.success'));
}
return { showNotification };
}
// Outside components (e.g., in stores)
import i18n from '@/i18n';
export function getErrorMessage(code: string) {
return i18n.global.t(`errors.${code}`);
}
Cross-Platform Best Practices {#best-practices}
1. Consistent Key Naming
Use the same key structure across all platforms:
Platform | Key Format
------------|------------------
Android | auth_login
React | auth.login
Vue | auth.login
Recommended: Use a shared source of truth
// shared/translations/en.json
{
"auth": {
"login": "Log In",
"logout": "Log Out"
}
}
Then transform for each platform:
// build-scripts/transform-android.js
function toAndroidXml(json) {
let xml = '<?xml version="1.0" encoding="utf-8"?>\n<resources>\n';
function flatten(obj, prefix = '') {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}_${key}` : key;
if (typeof value === 'string') {
xml += ` <string name="${fullKey}">${escapeXml(value)}</string>\n`;
} else {
flatten(value, fullKey);
}
}
}
flatten(json);
xml += '</resources>';
return xml;
}
2. Placeholder Standardization
Different frameworks use different placeholder syntax:
| Platform | Syntax | Example |
|---|---|---|
| Android | %1$s, %2$d | Hello, %1$s! |
| i18next | {{name}} | Hello, {{name}}! |
| Vue i18n | {name} | Hello, {name}! |
| ICU | {name} | Hello, {name}! |
Solution: Use ICU Message Format as source
{
"greeting": "Hello, {name}!"
}
Transform during build:
// For Android
"Hello, %1$s!"
// For i18next
"Hello, {{name}}!"
3. Pluralization Strategy
Use ICU plural format as the standard:
{
"items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
4. Context for Translators
Always provide context:
{
"play": "Play",
"_play.context": "Button to start video playback",
"play_game": "Play",
"_play_game.context": "Button to start a game"
}
For Android XML:
<string name="play" comment="Button to start video playback">Play</string>
5. Avoid String Concatenation
// ❌ Bad - word order varies by language
const msg = t('hello') + ' ' + name + '!';
// ✅ Good
const msg = t('greeting', { name });
6. Handle Text Expansion
German text is ~30% longer than English. Design for flexibility:
/* Allow buttons to grow */
.button {
min-width: 100px;
width: auto;
padding: 8px 16px;
}
7. RTL Support
Always design with RTL in mind:
/* Use logical properties */
.card {
margin-inline-start: 16px; /* Not margin-left */
padding-inline-end: 8px; /* Not padding-right */
}
<!-- Android: Use start/end instead of left/right -->
<TextView
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp" />
Common Pitfalls and How to Avoid Them {#pitfalls}
Pitfall 1: Missing Fallback Language
Problem: App crashes when translation is missing.
Solution:
// React i18next
i18n.init({
fallbackLng: 'en',
saveMissing: true,
missingKeyHandler: (lng, ns, key) => {
console.warn(`Missing translation: ${key}`);
}
});
Pitfall 2: Hardcoded Strings in Code
Problem: Strings scattered throughout the codebase.
Solution: Use linting rules:
// .eslintrc
{
"rules": {
"i18next/no-literal-string": "error"
}
}
Pitfall 3: Date/Number Formatting
Problem: Using string concatenation for dates.
Solution: Use Intl API:
// Dates
const formatted = new Intl.DateTimeFormat(locale, {
dateStyle: 'long'
}).format(date);
// Numbers
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'EUR'
}).format(19.99);
Pitfall 4: Incomplete Translations
Problem: Shipping with missing translations.
Solution: Add CI checks:
# Check for missing translations
npx i18next-parser --fail-on-warnings
Pitfall 5: Translation in Wrong Context
Problem: Same word, different meaning ("Save" as in save file vs save money).
Solution: Use unique keys with context:
{
"save_file": "Save",
"save_money": "Save",
"post_noun": "Post",
"post_verb": "Post"
}
Pitfall 6: Gendered Language
Problem: Languages like German have grammatical gender.
Solution: Use ICU SelectFormat:
{
"welcome": "{gender, select, male {Willkommen, Herr {name}} female {Willkommen, Frau {name}} other {Willkommen, {name}}}"
}
Translation Management Workflow {#workflow}
Recommended Workflow
1. Developer adds key → "auth.login": "Log In"
2. Push to repository → Git commit
3. CI extracts strings → i18next-parser / formatjs extract
4. Sync to TMS → Crowdin / Lokalise / Phrase
5. Translators work → In TMS interface
6. Auto-sync back → Webhook / CI job
7. Deploy with new → Build includes translations
translations
Popular Translation Management Systems
| Tool | Best For | Pricing |
|---|---|---|
| Crowdin | Open source, large teams | Free for OSS |
| Lokalise | Developer experience | From $120/mo |
| Phrase | Enterprise | From $25/mo |
| POEditor | Small teams | Free tier available |
| Transifex | Large projects | From $99/mo |
CI Integration Example
# .github/workflows/i18n.yml
name: i18n Sync
on:
push:
paths:
- 'src/i18n/locales/**'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract new keys
run: npx i18next-parser
- name: Upload to Crowdin
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
Library Comparison {#comparison}
React i18n Libraries
| Library | Bundle Size | Features | Best For |
|---|---|---|---|
| i18next | 42kb | Full-featured, plugins | Most projects |
| react-intl | 35kb | ICU format, formatjs | Enterprise |
| LinguiJS | 5kb | Minimal, compile-time | Performance |
Vue i18n Libraries
| Library | Bundle Size | Features | Best For |
|---|---|---|---|
| vue-i18n | 30kb | Official, full-featured | Most projects |
| vue-i18next | 42kb | i18next ecosystem | Cross-platform |
| fluent-vue | 15kb | Mozilla Fluent | Complex grammar |
Android i18n Options
| Approach | Complexity | Best For |
|---|---|---|
| strings.xml | Low | Standard apps |
| JSON + Custom | Medium | Dynamic content |
| ICU4J | High | Complex formatting |
Quick Start Checklist
- Set up translation file structure
- Configure fallback language
- Implement language switcher
- Add RTL support
- Set up pluralization
- Configure date/number formatting
- Add missing translation warnings
- Set up CI extraction
- Connect translation management tool
- Document translation process for team
Conclusion
Implementing i18n correctly from the start saves countless hours later. Key takeaways:
- Choose the right format: JSON for web, XML for Android native
- Standardize across platforms: Use ICU format as source of truth
- Avoid common pitfalls: No hardcoding, no concatenation
- Automate the workflow: CI/CD integration with TMS
- Test with real languages: Don't just test with English
Whether you're building for Android, React, or Vue, following these best practices ensures your app can scale globally with minimal friction.

