Examine how AI tools hardcode strings, ignore RTL languages, and mishandle date/time formatting, currency display, and pluralization rules. Learn react-i18next, ICU message format, and pseudo-localization testing.
Introduction: The English-Centric Problem
AI code generators are trained predominantly on English-language codebases, creating a systemic bias that makes internationalization an afterthought. When AI generates UI code, it hardcodes strings, ignores right-to-left languages, and produces date/time formats that only work in the US.
Key Statistics
- 2-3x cost to retrofit i18n after launch
- 20-25% German text expansion over English
- 2.1M weekly downloads for react-i18next
- 197+ hardcoded strings found in Gemini CLI
An analysis of the Gemini CLI codebase identified approximately 197 user-facing text strings that would benefit from internationalization. All UI text was hardcoded in English, creating barriers for non-English speakers. This pattern is common in AI-generated code.
Why AI Fails at Internationalization
Training Data Bias
AI models learn from public codebases that are predominantly English-centric:
- Hardcoded English strings: Tutorial code and examples rarely use i18n libraries
- LTR-only layouts: CSS examples use left/right instead of logical properties
- US date formats: AI defaults to MM/DD/YYYY, causing confusion globally
- Simple pluralization: English only has "one" and "other" forms, but many languages have 4-6 plural categories
- Concatenated strings: AI builds sentences by joining strings, breaking word order in other languages
Common AI i18n Mistakes
- Text: AI uses
"Welcome, " + name; proper i18n usest('welcome', { name }) - Pluralization: AI uses
count + " item" + (count !== 1 ? "s" : ""); proper i18n uses ICU plural format - Dates: AI uses
date.toLocaleDateString()without locale; proper i18n usesIntl.DateTimeFormat(locale) - Layout: AI uses
margin-left: 10px; proper i18n usesmargin-inline-start: 10px - Direction: AI provides no RTL support; proper i18n uses
dir="rtl"+ logical CSS
Hardcoded Strings: The Root Problem
Hardcoded strings make localization extremely difficult. Translators must read code to find translatable text, and consistency across the application becomes impossible to maintain.
// AI-Generated: Hardcoded Strings - untranslatable
function ProductCard({ product }) {
return (
<div className="product-card">
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
<p>{product.inStock ? "In Stock" : "Out of Stock"}</p>
<button>Add to Cart</button>
<p>
{product.reviews} {product.reviews === 1 ? "review" : "reviews"}
</p>
</div>
);
}
// Problems:
// 1. All strings hardcoded in English
// 2. Price format won't work outside US
// 3. Pluralization only works in English
// 4. Word order fixed (won't work in German, Japanese)
// Proper: Externalized Strings with i18n
import { useTranslation } from 'react-i18next';
function ProductCard({ product }) {
const { t } = useTranslation();
return (
<div className="product-card">
<h2>{product.name}</h2>
<p>
{t('product.price', {
price: product.price,
formatParams: { price: { style: 'currency', currency: 'USD' } }
})}
</p>
<p>{t(product.inStock ? 'product.inStock' : 'product.outOfStock')}</p>
<button>{t('product.addToCart')}</button>
<p>{t('product.reviews', { count: product.reviews })}</p>
</div>
);
}
// Translation files:
// en.json
{
"product": {
"price": "Price: {{price, currency}}",
"inStock": "In Stock",
"outOfStock": "Out of Stock",
"addToCart": "Add to Cart",
"reviews": "{{count}} review",
"reviews_plural": "{{count}} reviews"
}
}
RTL Support with CSS Logical Properties
Right-to-left (RTL) scripts are used by Arabic, Hebrew, Persian, Urdu, and other languages. AI-generated CSS uses physical properties like left and right, which break RTL layouts.
HTML Setup for RTL
<!-- Set direction and language on html element -->
<html dir="rtl" lang="ar">
<!-- Or dynamically with JavaScript -->
<script>
function setDirection(locale) {
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'ps', 'sd', 'yi'];
const isRTL = rtlLocales.some(rtl => locale.startsWith(rtl));
document.documentElement.dir = isRTL ? 'rtl' : 'ltr';
document.documentElement.lang = locale;
}
</script>
CSS Logical Properties
Logical properties automatically flip based on the document direction. Use inline for the text flow axis (left/right in LTR) and block for the perpendicular axis (top/bottom).
margin-leftbecomesmargin-inline-startmargin-rightbecomesmargin-inline-endpadding-leftbecomespadding-inline-startleft(position) becomesinset-inline-starttext-align: leftbecomestext-align: start
/* AI-Generated: Physical Properties (Breaks RTL) */
.sidebar {
position: fixed;
left: 0;
width: 250px;
}
.content {
margin-left: 250px;
padding-left: 20px;
text-align: left;
}
.back-button::before {
content: "..."; /* Arrow points wrong way in RTL */
}
/* Proper: Logical Properties (Works for LTR and RTL) */
.sidebar {
position: fixed;
inset-inline-start: 0;
width: 250px;
}
.content {
margin-inline-start: 250px;
padding-inline-start: 20px;
text-align: start;
}
/* Flip icons for RTL */
[dir="rtl"] .back-button::before {
transform: scaleX(-1);
}
/* Flexbox and Grid automatically respect direction */
.nav-items {
display: flex;
gap: 1rem;
justify-content: flex-start;
}
JavaScript Intl API for Formatting
The Intl object provides locale-sensitive formatting for dates, numbers, currencies, and more. It's built into JavaScript and eliminates the need for heavy formatting libraries.
Date and Time Formatting
// Intl.DateTimeFormat - locale-aware date formatting
const date = new Date('2025-01-24T14:30:00');
// US English
new Intl.DateTimeFormat('en-US').format(date);
// "1/24/2025"
// British English
new Intl.DateTimeFormat('en-GB').format(date);
// "24/01/2025"
// German
new Intl.DateTimeFormat('de-DE').format(date);
// "24.1.2025"
// Japanese
new Intl.DateTimeFormat('ja-JP').format(date);
// "2025/1/24"
// Relative time formatting
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(2, 'week'); // "in 2 weeks"
rtf.format(-3, 'month'); // "3 months ago"
Number and Currency Formatting
// Intl.NumberFormat - locale-aware number formatting
const price = 1234567.89;
// US English
new Intl.NumberFormat('en-US').format(price);
// "1,234,567.89"
// German (uses . for thousands, , for decimal)
new Intl.NumberFormat('de-DE').format(price);
// "1.234.567,89"
// Currency formatting
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
// "$1,234,567.89"
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(price);
// "..1,234,568" (JPY has no decimal places)
// Compact notation (1K, 1M, etc.)
new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short'
}).format(1500000);
// "1.5M"
ICU Message Format and Pluralization
ICU MessageFormat is the industry standard for handling complex translations including pluralization, gender, and select statements. It uses CLDR (Common Locale Data Repository) rules that define plural categories for each language.
Pluralization Challenges
English only has two plural forms (one, other), but other languages are more complex:
- English: one, other (1 item, 2 items)
- French: one, many, other
- Russian: one, few, many, other (1 file, 2 files, 5 files, 21 file)
- Arabic: zero, one, two, few, many, other
- Polish: one, few, many, other
ICU Message Syntax
// Basic plural syntax
// {variableName, plural, matches}
// English translation
{
"items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
// Russian translation (note different plural forms)
{
"items": "{count, plural, =0 {No files} one {# file} few {# files} many {# files} other {# files}}"
}
// Examples of output:
// count=0 -> "No items"
// count=1 -> "1 item"
// count=5 -> "5 items"
// count=21 (Russian) -> "21 file" (one form, not other!)
Advanced ICU Patterns
// Select for gender or other categories
{
"invitation": "{gender, select, male {He invited you} female {She invited you} other {They invited you}}"
}
// Nested plural and select
{
"notification": "{gender, select,
male {{count, plural, one {He has # notification} other {He has # notifications}}}
female {{count, plural, one {She has # notification} other {She has # notifications}}}
other {{count, plural, one {They have # notification} other {They have # notifications}}}
}"
}
// Offset for "X and N others" pattern
{
"likes": "{count, plural, offset:1
=0 {No one liked this}
=1 {You liked this}
one {You and one other person liked this}
other {You and # others liked this}
}"
}
React i18n: react-i18next vs react-intl
Two libraries dominate React internationalization: react-i18next (2.1M weekly downloads) and react-intl (FormatJS). Both are excellent, but serve different needs.
Comparison
- Downloads: react-i18next 2.1M vs react-intl ~1M
- Message Format: react-i18next supports ICU + custom; react-intl is strict ICU
- Namespaces: react-i18next has full support with lazy loading; react-intl is limited
- Best For: react-i18next for large apps with flexibility; react-intl for enterprise TMS integration
react-i18next Setup
// i18n.ts - Configuration
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
i18n
.use(Backend) // Load translations from /locales
.use(LanguageDetector) // Detect user language
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'de', 'fr', 'ar', 'ja'],
// Namespaces for code splitting
ns: ['common', 'products', 'checkout'],
defaultNS: 'common',
interpolation: {
escapeValue: false, // React already escapes
},
// Backend options for lazy loading
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
// Language detection
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
caches: ['cookie', 'localStorage'],
},
});
export default i18n;
// Usage in components
import { useTranslation, Trans } from 'react-i18next';
function ProductPage({ product }) {
const { t, i18n } = useTranslation(['products', 'common']);
return (
<div>
<h1>{t('products:title')}</h1>
{/* Simple translation */}
<p>{t('products:description', { name: product.name })}</p>
{/* Pluralization */}
<p>{t('products:reviews', { count: product.reviewCount })}</p>
{/* Trans component for complex markup */}
<Trans i18nKey="products:terms">
By purchasing, you agree to our <a href="/terms">Terms</a>
and <a href="/privacy">Privacy Policy</a>.
</Trans>
{/* Language switcher */}
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="ar">Arabic</option>
</select>
</div>
);
}
Pseudo-Localization Testing
Pseudo-localization transforms your source text into an altered "fake language" to catch i18n issues before paying for real translation. It's one of the most effective ways to find hardcoded strings and layout problems.
What Pseudo-Localization Detects
- Hardcoded strings: Unaccented text reveals untranslated strings
- Layout issues: Text expansion (30-100%) shows clipping and overflow
- Encoding problems: Accented characters reveal UTF-8 issues
- Truncation: Missing brackets show cut-off text
- Concatenation: Multiple bracket pairs reveal string joining
Pseudo-Localization Example
Original: "Welcome to our store"
Pseudo: "[Welcome to our store one two three]"
If you see unaccented text like "Submit" instead of "[Submit one]", you know that string is hardcoded.
Implementing Pseudo-Localization
// pseudo-localize.ts
const ACCENTS: Record<string, string> = {
'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f',
'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
// ... more mappings
};
interface PseudoOptions {
expansionFactor?: number; // 1.3 = 30% longer
addBrackets?: boolean;
preservePlaceholders?: boolean;
}
function pseudoLocalize(text: string, options: PseudoOptions = {}): string {
const {
expansionFactor = 1.3,
addBrackets = true,
preservePlaceholders = true
} = options;
// Preserve placeholders like {name} or {{count}}
const placeholderRegex = /(\{\{?\w+\}?\})/g;
const placeholders: string[] = [];
let processedText = text;
if (preservePlaceholders) {
processedText = text.replace(placeholderRegex, (match) => {
placeholders.push(match);
return `__PLACEHOLDER_${placeholders.length - 1}__`;
});
}
// Apply accents
let result = processedText
.split('')
.map(char => ACCENTS[char] || char)
.join('');
// Restore placeholders
placeholders.forEach((placeholder, index) => {
result = result.replace(`__PLACEHOLDER_${index}__`, placeholder);
});
// Add expansion (pad words)
const expansionPadding = Math.ceil(result.length * (expansionFactor - 1) / 3);
const padding = ' one two three four five six'.slice(0, expansionPadding);
result = result + padding;
// Add brackets for visibility
if (addBrackets) {
result = `[${result}]`;
}
return result;
}
// Usage
const enMessages = {
'welcome': 'Welcome, {name}!',
'items': '{count, plural, one {# item} other {# items}}',
'checkout': 'Proceed to Checkout',
};
const pseudoMessages = generatePseudoLocale(enMessages);
Translation Management Workflow
A proper i18n workflow separates translation from development, enables continuous localization, and integrates with CI/CD.
Recommended Workflow
// Project structure
/locales
/en
common.json # Shared translations
products.json # Product-specific
checkout.json # Checkout flow
/de
common.json
products.json
checkout.json
/ar
common.json
products.json
checkout.json
// Source of truth: English files
// Translations: Managed in TMS, synced via CI/CD
CI/CD Integration
# .github/workflows/i18n.yml
name: i18n Sync
on:
push:
paths:
- 'locales/en/**'
schedule:
- cron: '0 */6 * * *' # Every 6 hours
jobs:
sync-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Push new strings to TMS
if: github.event_name == 'push'
run: |
npx lokalise2 file upload \
--token ${{ secrets.LOKALISE_TOKEN }} \
--project-id ${{ secrets.LOKALISE_PROJECT_ID }} \
--file locales/en/common.json \
--lang-iso en
- name: Pull translations from TMS
run: |
npx lokalise2 file download \
--token ${{ secrets.LOKALISE_TOKEN }} \
--project-id ${{ secrets.LOKALISE_PROJECT_ID }} \
--format json \
--dest locales
- name: Create PR with updated translations
uses: peter-evans/create-pull-request@v5
with:
commit-message: 'chore(i18n): update translations'
title: 'Update translations from Lokalise'
branch: i18n/update-translations
Key Takeaways
Remember These Points
- Never hardcode strings: Always externalize text to translation files from the start
- Use CSS logical properties: Replace left/right with inline-start/inline-end for RTL support
- Use the Intl API: Built-in locale-aware formatting for dates, numbers, and currencies
- Learn ICU MessageFormat: Essential for proper pluralization beyond simple English rules
- Choose react-i18next or react-intl: Both excellent; i18next for flexibility, intl for standards
- Implement pseudo-localization: Catches hardcoded strings and layout issues before translation
- Integrate i18n in CI/CD: Automate string extraction and translation syncing
- Plan for text expansion: German is 20-25% longer; design flexible layouts
Conclusion
Internationalization isn't just about translation—it's about building applications that respect the linguistic, cultural, and directional differences of your global users. AI code generators, trained predominantly on English-centric codebases, consistently produce code that fails for non-English audiences.
The cost of retrofitting i18n after launch is 2-3 times higher than building it in from the start. By using libraries like react-i18next, understanding ICU MessageFormat for proper pluralization, implementing CSS logical properties for RTL support, and leveraging the built-in JavaScript Intl API, you can build applications that work for everyone.
Pseudo-localization testing catches hardcoded strings and layout issues before you invest in real translation. Combined with a proper translation management workflow integrated into CI/CD, you can maintain a truly international application without slowing down development.
Remember: every hardcoded string is a barrier for someone trying to use your application. Build for the world from day one.