406 lines
11 KiB
Markdown
406 lines
11 KiB
Markdown
|
|
# Advanced i18n Example - Multi-Locale Production Workflows
|
|||
|
|
|
|||
|
|
**Production-grade internationalization with 9+ locales, advanced translation features, and RTL support**
|
|||
|
|
|
|||
|
|
This example demonstrates building truly global applications with complex translation scenarios: pluralization rules, gender-aware strings, number/date formatting, fallback chains, and right-to-left layout support.
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
Basic i18n: English + Spanish + French
|
|||
|
|
Advanced i18n: 9 locales with complex rules, fallbacks, and region-specific formatting
|
|||
|
|
|
|||
|
|
**Key features:**
|
|||
|
|
- **9 complete locales** - Every form fully translated
|
|||
|
|
- **Advanced Fluent features** - Plurals, gender, context-aware strings
|
|||
|
|
- **Fallback chains** - Portuguese Brazil → Portuguese → Spanish → English
|
|||
|
|
- **RTL support** - Arabic with proper text direction
|
|||
|
|
- **Locale-specific formatting** - Currency ($1,234.56 vs 1.234,56€), dates, numbers
|
|||
|
|
- **Production workflow** - Translation maintenance, coverage tracking, testing
|
|||
|
|
|
|||
|
|
## Locales Included
|
|||
|
|
|
|||
|
|
| Locale | Region | Features | Currency | Plurals |
|
|||
|
|
|--------|--------|----------|----------|---------|
|
|||
|
|
| **en-US** | United States | Base reference | $ USD | 2 (1 vs other) |
|
|||
|
|
| **en-GB** | United Kingdom | British English, £ | £ GBP | 2 (1 vs other) |
|
|||
|
|
| **es-ES** | Spain | Gender agreement, € | € EUR | 2 (1 vs other) |
|
|||
|
|
| **es-MX** | Mexico | Gender agreement, $ | $ MXN | 2 (1 vs other) |
|
|||
|
|
| **pt-BR** | Brazil | Fallback: pt → es | R$ BRL | 2 (1 vs other) |
|
|||
|
|
| **pt-PT** | Portugal | Fallback: pt → es | € EUR | 2 (1 vs other) |
|
|||
|
|
| **fr-FR** | France | Gender agreement, € | € EUR | 2 (0/1 vs other) |
|
|||
|
|
| **ja-JP** | Japan | No plurals, no gender | ¥ JPY | 1 (no plurals) |
|
|||
|
|
| **ar-SA** | Saudi Arabia | RTL text direction, complex plurals | ﷼ SAR | 6 (0, 1, 2, 3-10, 11-99, 100+) |
|
|||
|
|
|
|||
|
|
## Directory Structure
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
17-advanced-i18n/
|
|||
|
|
├── README.md (This file)
|
|||
|
|
├── checkout-form.toml (Example form)
|
|||
|
|
├── test-locales.sh (Test all 9 locales)
|
|||
|
|
├── translations-status.md (Coverage matrix)
|
|||
|
|
├── i18n-config.toml (Fallback chains)
|
|||
|
|
└── locales/
|
|||
|
|
├── en-US/main.ftl (English US - reference)
|
|||
|
|
├── en-GB/main.ftl (English UK)
|
|||
|
|
├── es-ES/main.ftl (Spanish Spain)
|
|||
|
|
├── es-MX/main.ftl (Spanish Mexico)
|
|||
|
|
├── pt-BR/main.ftl (Portuguese Brazil)
|
|||
|
|
├── pt-PT/main.ftl (Portuguese Portugal)
|
|||
|
|
├── fr-FR/main.ftl (French)
|
|||
|
|
├── ja-JP/main.ftl (Japanese)
|
|||
|
|
└── ar-SA/main.ftl (Arabic - RTL)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Example Form: E-Commerce Checkout
|
|||
|
|
|
|||
|
|
`checkout-form.toml` demonstrates real-world translation challenges:
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
[[sections]]
|
|||
|
|
title = "Order Summary"
|
|||
|
|
|
|||
|
|
[[sections.elements]]
|
|||
|
|
name = "item_count"
|
|||
|
|
type = "display"
|
|||
|
|
label = "{ $count ->
|
|||
|
|
[one] You have 1 item in your cart
|
|||
|
|
*[other] You have { $count } items in your cart
|
|||
|
|
}"
|
|||
|
|
|
|||
|
|
[[sections.elements]]
|
|||
|
|
name = "subtotal"
|
|||
|
|
type = "display"
|
|||
|
|
label = "Subtotal: { $amount -number }"
|
|||
|
|
|
|||
|
|
[[sections.elements]]
|
|||
|
|
name = "tax"
|
|||
|
|
type = "display"
|
|||
|
|
label = "Tax: { $amount -currency }"
|
|||
|
|
|
|||
|
|
[[sections.elements]]
|
|||
|
|
name = "delivery_date"
|
|||
|
|
type = "display"
|
|||
|
|
label = "Estimated delivery: { $date -date }"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Each locale renders this form correctly:
|
|||
|
|
- **English:** "You have 5 items" | "Subtotal: $1,234.56" | "Estimated delivery: Tuesday, January 15, 2025"
|
|||
|
|
- **French:** "Vous avez 5 articles" | "Total: 1.234,56 €" | "Livraison estimée: mardi 15 janvier 2025"
|
|||
|
|
- **Arabic:** "لديك 5 عناصر" (RTL) | "الإجمالي: 1.234,56 ﷼" | "التسليم المتوقع: الثلاثاء 15 يناير 2025"
|
|||
|
|
|
|||
|
|
## Running Examples
|
|||
|
|
|
|||
|
|
### Test All Locales
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Run form in each locale (9 iterations)
|
|||
|
|
./test-locales.sh
|
|||
|
|
|
|||
|
|
# Output shows how form renders in each language
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Test Specific Locale
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# English US (default)
|
|||
|
|
cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
|
|||
|
|
# English UK
|
|||
|
|
LANG=en_GB cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
|
|||
|
|
# Spanish Spain
|
|||
|
|
LANG=es_ES cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
|
|||
|
|
# Arabic (RTL)
|
|||
|
|
LANG=ar_SA cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
|
|||
|
|
# French
|
|||
|
|
LANG=fr_FR cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
|
|||
|
|
# Japanese
|
|||
|
|
LANG=ja_JP cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
|
|||
|
|
# Portuguese Brazil (tests fallback chain)
|
|||
|
|
LANG=pt_BR cargo run -p typedialog-tui -- checkout-form.toml
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Check Translation Status
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# See which strings are translated in each locale
|
|||
|
|
cat translations-status.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Fluent Features Demonstrated
|
|||
|
|
|
|||
|
|
### 1. Pluralization
|
|||
|
|
|
|||
|
|
**English (2 plural forms):**
|
|||
|
|
```fluent
|
|||
|
|
items-in-cart =
|
|||
|
|
{ $count ->
|
|||
|
|
[one] You have 1 item
|
|||
|
|
*[other] You have { $count } items
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**French (special rule for 0 and 1):**
|
|||
|
|
```fluent
|
|||
|
|
articles-panier =
|
|||
|
|
{ $count ->
|
|||
|
|
[0] Vous n'avez pas d'articles
|
|||
|
|
[1] Vous avez 1 article
|
|||
|
|
*[other] Vous avez { $count } articles
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Arabic (6 plural forms):**
|
|||
|
|
```fluent
|
|||
|
|
items-in-cart =
|
|||
|
|
{ $count ->
|
|||
|
|
[0] لا توجد عناصر
|
|||
|
|
[1] عنصر واحد
|
|||
|
|
[2] عنصرين
|
|||
|
|
[3] { $count } عناصر
|
|||
|
|
[4] { $count } عنصر
|
|||
|
|
*[other] { $count } عنصر
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Gender Agreement
|
|||
|
|
|
|||
|
|
**Spanish (nouns have gender):**
|
|||
|
|
```fluent
|
|||
|
|
# -gender attribute determines which form to use
|
|||
|
|
guest-welcome = { $name, $gender ->
|
|||
|
|
[m] Bienvenido, { $name }
|
|||
|
|
[f] Bienvenida, { $name }
|
|||
|
|
*[other] Bienvenido, { $name }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**French:**
|
|||
|
|
```fluent
|
|||
|
|
thank-you = { $gender ->
|
|||
|
|
[m] Merci, Monsieur
|
|||
|
|
[f] Merci, Madame
|
|||
|
|
*[other] Merci
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Number Formatting
|
|||
|
|
|
|||
|
|
```fluent
|
|||
|
|
# Numbers format per locale automatically
|
|||
|
|
order-total = Total: { $amount -number }
|
|||
|
|
|
|||
|
|
# Output:
|
|||
|
|
# en-US: Total: 1,234.56
|
|||
|
|
# fr-FR: Total: 1.234,56
|
|||
|
|
# de-DE: Total: 1.234,56
|
|||
|
|
# ar-SA: Total: ١٬٢٣٤٫٥٦
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. Date Formatting
|
|||
|
|
|
|||
|
|
```fluent
|
|||
|
|
# Dates format per locale
|
|||
|
|
ship-date = Ships on { $date -date }
|
|||
|
|
|
|||
|
|
# Output:
|
|||
|
|
# en-US: Ships on 1/15/2025
|
|||
|
|
# fr-FR: Ships on 15/01/2025
|
|||
|
|
# ja-JP: Ships on 2025年1月15日
|
|||
|
|
# ar-SA: Ships on ١٥ يناير ٢٠٢٥
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. Fallback Chains
|
|||
|
|
|
|||
|
|
**Portuguese Brazil → Portuguese → Spanish → English**
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
# i18n-config.toml
|
|||
|
|
fallback_chains = [
|
|||
|
|
"pt-BR → pt → es → en-US",
|
|||
|
|
"pt-PT → pt → es → en-US",
|
|||
|
|
"es-MX → es → en-US",
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
If a string is missing from `pt-BR/main.ftl`:
|
|||
|
|
1. Look in `pt-PT/main.ftl`
|
|||
|
|
2. Look in `es/main.ftl`
|
|||
|
|
3. Look in `en-US/main.ftl` (fallback to English)
|
|||
|
|
|
|||
|
|
## RTL (Right-to-Left) Support
|
|||
|
|
|
|||
|
|
**Arabic (ar-SA):**
|
|||
|
|
|
|||
|
|
```ftl
|
|||
|
|
# Direction attribute tells UI to render RTL
|
|||
|
|
welcome = مرحبا بك
|
|||
|
|
.direction = rtl
|
|||
|
|
|
|||
|
|
# Numerals use Arabic-Indic numerals
|
|||
|
|
order-number = رقم الطلب: { $number }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
TypeDialog automatically:
|
|||
|
|
- Reverses text direction
|
|||
|
|
- Handles number formatting (Arabic numerals: ٠١٢٣٤٥٦٧٨٩)
|
|||
|
|
- Mirrors UI layout
|
|||
|
|
|
|||
|
|
## Translation Maintenance
|
|||
|
|
|
|||
|
|
### Workflow
|
|||
|
|
|
|||
|
|
1. **Add new string in en-US:**
|
|||
|
|
```ftl
|
|||
|
|
new-feature = This is a new feature
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **Mark as untranslated in other locales:**
|
|||
|
|
```ftl
|
|||
|
|
new-feature = [NEEDS TRANSLATION]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **Update status tracking:**
|
|||
|
|
```bash
|
|||
|
|
./translations-status.md # Shows incomplete translations
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **Translate and remove marker:**
|
|||
|
|
```ftl
|
|||
|
|
new-feature = Ceci est une nouvelle fonctionnalité
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
5. **Verify coverage:**
|
|||
|
|
```bash
|
|||
|
|
grep -r "NEEDS TRANSLATION" locales/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Coverage Matrix
|
|||
|
|
|
|||
|
|
`translations-status.md` shows:
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
| Locale | Complete | Partial | Missing |
|
|||
|
|
|--------|----------|---------|---------|
|
|||
|
|
| en-US | 100% | 0% | 0% |
|
|||
|
|
| en-GB | 95% | 5% | 0% |
|
|||
|
|
| es-ES | 90% | 10% | 0% |
|
|||
|
|
| ... | ... | ... | ... |
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Testing Translations Programmatically
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Validate all Fluent files parse correctly
|
|||
|
|
for locale in locales/*/; do
|
|||
|
|
nickel check "$locale/main.ftl" || echo "Error in $locale"
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Test that all form keys are translated in all locales
|
|||
|
|
./test-locales.sh --validate
|
|||
|
|
|
|||
|
|
# Run form and verify output in each locale
|
|||
|
|
for locale in en-US en-GB es-ES pt-BR ar-SA ja-JP; do
|
|||
|
|
echo "Testing $locale..."
|
|||
|
|
LANG="$locale" cargo run -p typedialog -- checkout-form.toml --format json | jq . > /dev/null
|
|||
|
|
done
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Key Learning Points
|
|||
|
|
|
|||
|
|
### 1. Pluralization is Language-Specific
|
|||
|
|
|
|||
|
|
- English: 2 forms (singular 1 vs plural)
|
|||
|
|
- Polish: 4 forms (1, 2-4, 5-21, 22+)
|
|||
|
|
- Arabic: 6 forms (0, 1, 2, 3-10, 11-99, 100+)
|
|||
|
|
- Japanese: 1 form (no plurals)
|
|||
|
|
|
|||
|
|
**Cannot** use a one-size-fits-all plural function!
|
|||
|
|
|
|||
|
|
### 2. Gender Agreement Requires Context
|
|||
|
|
|
|||
|
|
Spanish/French/German nouns have gender, affecting:
|
|||
|
|
- Articles (el/la/lo, le/la/l', der/die/das)
|
|||
|
|
- Adjectives (rojo/roja, rouge/rouge, roter/rote)
|
|||
|
|
- Past participles (comido/comida, mangé/mangée)
|
|||
|
|
|
|||
|
|
**Cannot** just translate strings in isolation!
|
|||
|
|
|
|||
|
|
### 3. Number Formatting Varies
|
|||
|
|
|
|||
|
|
- US: 1,234.56 (comma thousands, period decimal)
|
|||
|
|
- Europe: 1.234,56 (period thousands, comma decimal)
|
|||
|
|
- India: 12,34,567 (special grouping)
|
|||
|
|
- Arabic: Uses Arabic-Indic numerals (٠١٢٣٤)
|
|||
|
|
|
|||
|
|
**Cannot** hardcode number formatting!
|
|||
|
|
|
|||
|
|
### 4. Date Formatting Varies
|
|||
|
|
|
|||
|
|
- en-US: 1/15/2025
|
|||
|
|
- en-GB: 15/01/2025
|
|||
|
|
- fr-FR: 15 janvier 2025
|
|||
|
|
- ja-JP: 2025年1月15日
|
|||
|
|
|
|||
|
|
**Cannot** use ISO format everywhere!
|
|||
|
|
|
|||
|
|
### 5. RTL Requires Special Handling
|
|||
|
|
|
|||
|
|
Right-to-left languages (Arabic, Hebrew, Persian):
|
|||
|
|
- Text flows right to left
|
|||
|
|
- Numbers still read left to right
|
|||
|
|
- UI layout must be mirrored
|
|||
|
|
|
|||
|
|
**Cannot** ignore RTL just because it's a minority use case!
|
|||
|
|
|
|||
|
|
## Real-World Best Practices
|
|||
|
|
|
|||
|
|
1. **Always use Fluent** for anything beyond trivial translations
|
|||
|
|
2. **Test with native speakers** - Automated tests miss context
|
|||
|
|
3. **Plan for fallbacks** - Incomplete translations should not crash
|
|||
|
|
4. **Track coverage** - Know which locales are complete
|
|||
|
|
5. **Separate translation work** - Translators shouldn't need to understand code
|
|||
|
|
6. **Use context** - Provide translator notes for ambiguous strings
|
|||
|
|
7. **Test all locales** - Every deployment should verify each language
|
|||
|
|
8. **Monitor in production** - Users will report issues you missed
|
|||
|
|
|
|||
|
|
## Troubleshooting
|
|||
|
|
|
|||
|
|
**"String appears in English instead of locale"**
|
|||
|
|
- Check if fallback chain applies (pt-BR → pt → es → en-US)
|
|||
|
|
- Verify LANG environment variable is set correctly
|
|||
|
|
- Check Fluent file for syntax errors
|
|||
|
|
|
|||
|
|
**"Numbers don't format correctly"**
|
|||
|
|
- Ensure you're using `-number` function in Fluent
|
|||
|
|
- Check locale data is available in your system
|
|||
|
|
- Test with: `date +%x` to verify locale
|
|||
|
|
|
|||
|
|
**"Arabic text appears reversed"**
|
|||
|
|
- Add `.direction = rtl` attribute in Fluent
|
|||
|
|
- Check UI component supports RTL
|
|||
|
|
- Verify numerals format with Arabic-Indic style
|
|||
|
|
|
|||
|
|
**"Plural forms incorrect"**
|
|||
|
|
- Verify plural rules for language (not just guess English 2-form)
|
|||
|
|
- Check documentation for language's plural rules
|
|||
|
|
- Test with edge cases: 0, 1, 2, 21, 100, 1000
|
|||
|
|
|
|||
|
|
## Next Steps
|
|||
|
|
|
|||
|
|
1. **Add more locales** - Use existing locales as templates
|
|||
|
|
2. **Extract all UI strings** - Audit your forms for hardcoded text
|
|||
|
|
3. **Set up translation platform** - Crowdin, Weblate, or similar
|
|||
|
|
4. **Automate testing** - Run `test-locales.sh` in CI/CD
|
|||
|
|
5. **Monitor translations** - Track completion and quality
|
|||
|
|
6. **Build management UI** - For non-developers to manage translations
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Key Takeaway:** Internationalization is not just translation - it's localizing numbers, dates, plurals, and gender agreement while handling RTL and fallback chains. Use Fluent for complex features and always test with native speakers.
|