How to Internationalize Your Application
This guide shows you how to use Metric Engine’s Babel integration to create applications that work across different locales and currencies.
Quick Setup
1. Install with Babel Support
pip install "metric-engine[babel]"
2. Basic Multi-Locale Example
from metricengine.factories import money, percent
from metricengine.policy import DisplayPolicy
# Your business data
revenue = money(1234567.89)
growth_rate = percent(12.5, input="percent")
profit_margin = percent(0.185, input="ratio") # 18.5%
# Define locale-specific policies
locales = {
"us": DisplayPolicy(locale="en_US", currency="USD"),
"uk": DisplayPolicy(locale="en_GB", currency="GBP"),
"germany": DisplayPolicy(locale="de_DE", currency="EUR"),
"japan": DisplayPolicy(locale="ja_JP", currency="JPY", max_frac=0),
}
# Format for each locale
for region, policy in locales.items():
print(f"\n{region.upper()} Formatting:")
print(f" Revenue: {revenue.format(policy)}")
print(f" Growth: {growth_rate.format(policy)}")
print(f" Margin: {profit_margin.format(policy)}")
Output:
US Formatting:
Revenue: $1,234,567.89
Growth: 12.50%
Margin: 18.50%
UK Formatting:
Revenue: £1,234,567.89
Growth: 12.50%
Margin: 18.50%
GERMANY Formatting:
Revenue: 1.234.567,89 €
Growth: 12,50 %
Margin: 18,50 %
JAPAN Formatting:
Revenue: ¥1,234,568
Growth: 13%
Margin: 19%
Building a Multi-Currency Dashboard
1. Create a Formatter Service
from typing import Dict
from metricengine.formatters.base import get_formatter
from metricengine.policy import DisplayPolicy
class LocalizationService:
"""Service for handling multi-locale formatting."""
def __init__(self):
self.formatter = get_formatter()
self.policies = self._create_policies()
def _create_policies(self) -> Dict[str, DisplayPolicy]:
"""Create standard policies for supported locales."""
return {
"en_US": DisplayPolicy(
locale="en_US",
currency="USD",
max_frac=2,
use_grouping=True
),
"en_GB": DisplayPolicy(
locale="en_GB",
currency="GBP",
max_frac=2,
use_grouping=True
),
"de_DE": DisplayPolicy(
locale="de_DE",
currency="EUR",
max_frac=2,
use_grouping=True
),
"fr_FR": DisplayPolicy(
locale="fr_FR",
currency="EUR",
max_frac=2,
use_grouping=True
),
"ja_JP": DisplayPolicy(
locale="ja_JP",
currency="JPY",
max_frac=0, # Yen doesn't use decimal places
use_grouping=True
),
}
def format_for_locale(self, value, locale: str):
"""Format a value for a specific locale."""
policy = self.policies.get(locale, self.policies["en_US"])
return value.format(policy)
def get_accounting_policy(self, locale: str) -> DisplayPolicy:
"""Get accounting-style policy for financial reports."""
base_policy = self.policies.get(locale, self.policies["en_US"])
return DisplayPolicy(
locale=base_policy.locale,
currency=base_policy.currency,
max_frac=base_policy.max_frac,
use_grouping=base_policy.use_grouping,
currency_style="accounting",
negative_parens=True
)
# Usage
localization = LocalizationService()
2. Create Financial Reports
from metricengine.factories import money
def generate_financial_report(data: dict, locale: str = "en_US"):
"""Generate a localized financial report."""
localization = LocalizationService()
accounting_policy = localization.get_accounting_policy(locale)
# Calculate key metrics
revenue = data["revenue"]
expenses = data["expenses"]
net_income = revenue - expenses
# Format with accounting style
report = f"""
Financial Report ({locale})
{'=' * 30}
Revenue: {revenue.format(accounting_policy)}
Expenses: {expenses.format(accounting_policy)}
Net Income: {net_income.format(accounting_policy)}
"""
return report
# Example usage
financial_data = {
"revenue": money(500000),
"expenses": money(350000)
}
# Generate reports for different locales
for locale in ["en_US", "de_DE", "ja_JP"]:
print(generate_financial_report(financial_data, locale))
Handling User Preferences
1. User Preference System
from dataclasses import dataclass
from typing import Optional
@dataclass
class UserPreferences:
"""User's localization preferences."""
locale: str = "en_US"
currency: str = "USD"
decimal_places: int = 2
use_accounting_style: bool = False
show_currency_symbol: bool = True
class UserLocalizer:
"""Handles user-specific formatting preferences."""
def __init__(self, preferences: UserPreferences):
self.preferences = preferences
self.formatter = get_formatter()
def create_policy(self) -> DisplayPolicy:
"""Create a display policy from user preferences."""
return DisplayPolicy(
locale=self.preferences.locale,
currency=self.preferences.currency,
max_frac=self.preferences.decimal_places,
min_frac=self.preferences.decimal_places,
use_grouping=True,
currency_style="accounting" if self.preferences.use_accounting_style else "standard",
negative_parens=self.preferences.use_accounting_style
)
def format_money(self, amount):
"""Format money according to user preferences."""
policy = self.create_policy()
return amount.format(policy)
def format_percentage(self, percentage):
"""Format percentage according to user preferences."""
policy = self.create_policy()
return percentage.format(policy)
# Example usage
user_prefs = UserPreferences(
locale="de_DE",
currency="EUR",
decimal_places=2,
use_accounting_style=True
)
localizer = UserLocalizer(user_prefs)
amount = money(1234.56)
print(localizer.format_money(amount)) # 1.234,56 €
2. Dynamic Locale Switching
class DynamicFormatter:
"""Formatter that can switch locales dynamically."""
def __init__(self):
self.formatter = get_formatter()
self.current_locale = "en_US"
self.policies_cache = {}
def set_locale(self, locale: str, currency: str = None):
"""Switch to a different locale."""
self.current_locale = locale
if currency:
# Update cached policy for this locale
self.policies_cache[locale] = DisplayPolicy(
locale=locale,
currency=currency,
max_frac=2,
use_grouping=True
)
def get_policy(self) -> DisplayPolicy:
"""Get policy for current locale."""
if self.current_locale not in self.policies_cache:
# Create default policy for locale
currency_map = {
"en_US": "USD", "en_GB": "GBP", "de_DE": "EUR",
"fr_FR": "EUR", "ja_JP": "JPY", "zh_CN": "CNY"
}
currency = currency_map.get(self.current_locale, "USD")
self.policies_cache[self.current_locale] = DisplayPolicy(
locale=self.current_locale,
currency=currency,
max_frac=2 if currency != "JPY" else 0,
use_grouping=True
)
return self.policies_cache[self.current_locale]
def format_value(self, value):
"""Format value with current locale."""
policy = self.get_policy()
return value.format(policy)
# Usage example
formatter = DynamicFormatter()
amount = money(1234.56)
# Switch between locales
locales = ["en_US", "de_DE", "ja_JP", "fr_FR"]
for locale in locales:
formatter.set_locale(locale)
print(f"{locale}: {formatter.format_value(amount)}")
Web Application Integration
1. Flask Example
from flask import Flask, request, session
from metricengine.factories import money, percent
from metricengine.policy import DisplayPolicy
app = Flask(__name__)
app.secret_key = 'your-secret-key'
def get_user_locale():
"""Get user's preferred locale from session or browser."""
return session.get('locale', request.accept_languages.best_match(['en_US', 'de_DE', 'fr_FR', 'ja_JP']))
def create_display_policy(locale: str) -> DisplayPolicy:
"""Create display policy for locale."""
currency_map = {
'en_US': 'USD', 'de_DE': 'EUR',
'fr_FR': 'EUR', 'ja_JP': 'JPY'
}
return DisplayPolicy(
locale=locale,
currency=currency_map.get(locale, 'USD'),
max_frac=2 if locale != 'ja_JP' else 0,
use_grouping=True
)
@app.route('/dashboard')
def dashboard():
"""Display localized financial dashboard."""
locale = get_user_locale()
policy = create_display_policy(locale)
# Sample financial data
revenue = money(1500000)
expenses = money(1200000)
profit = revenue - expenses
margin = percent(0.20, input="ratio")
return f"""
<h1>Financial Dashboard</h1>
<p>Locale: {locale}</p>
<ul>
<li>Revenue: {revenue.format(policy)}</li>
<li>Expenses: {expenses.format(policy)}</li>
<li>Profit: {profit.format(policy)}</li>
<li>Margin: {margin.format(policy)}</li>
</ul>
"""
@app.route('/set_locale/<locale>')
def set_locale(locale):
"""Allow users to change their locale preference."""
session['locale'] = locale
return f"Locale set to {locale}"
2. Django Integration
# views.py
from django.shortcuts import render
from django.utils import translation
from metricengine.factories import money, percent
from metricengine.policy import DisplayPolicy
def get_display_policy(request):
"""Create display policy from Django's locale system."""
locale = translation.get_language()
# Map Django locale codes to display policies
locale_map = {
'en': DisplayPolicy(locale='en_US', currency='USD'),
'de': DisplayPolicy(locale='de_DE', currency='EUR'),
'fr': DisplayPolicy(locale='fr_FR', currency='EUR'),
'ja': DisplayPolicy(locale='ja_JP', currency='JPY', max_frac=0),
}
lang_code = locale.split('-')[0] # 'en-us' -> 'en'
return locale_map.get(lang_code, locale_map['en'])
def financial_report(request):
"""Generate localized financial report."""
policy = get_display_policy(request)
# Your business logic here
data = {
'revenue': money(2000000).format(policy),
'profit': money(400000).format(policy),
'margin': percent(0.20, input="ratio").format(policy),
}
return render(request, 'financial_report.html', data)
Testing Internationalization
1. Test Multiple Locales
import pytest
from metricengine.factories import money, percent
from metricengine.policy import DisplayPolicy
class TestInternationalization:
"""Test formatting across different locales."""
@pytest.mark.parametrize("locale,currency,expected_symbol", [
("en_US", "USD", "$"),
("de_DE", "EUR", "€"),
("ja_JP", "JPY", "¥"),
("en_GB", "GBP", "£"),
])
def test_currency_symbols(self, locale, currency, expected_symbol):
"""Test that currency symbols appear correctly."""
amount = money(100)
policy = DisplayPolicy(locale=locale, currency=currency)
result = amount.format(policy)
assert expected_symbol in result
def test_decimal_separators(self):
"""Test locale-specific decimal separators."""
amount = money(1234.56)
# US uses period
us_policy = DisplayPolicy(locale="en_US", currency="USD")
us_result = amount.format(us_policy)
assert "1,234.56" in us_result
# Germany uses comma
de_policy = DisplayPolicy(locale="de_DE", currency="EUR")
de_result = amount.format(de_policy)
assert "1.234,56" in de_result
def test_percentage_formatting(self):
"""Test percentage formatting across locales."""
rate = percent(15.5, input="percent")
locales = ["en_US", "de_DE", "fr_FR"]
for locale in locales:
policy = DisplayPolicy(locale=locale)
result = rate.format(policy)
assert "15" in result
assert "%" in result
2. Fallback Testing
def test_babel_fallback():
"""Test that formatting works without Babel."""
# Mock Babel as unavailable
import sys
babel_modules = [mod for mod in sys.modules if mod.startswith('babel')]
for mod in babel_modules:
del sys.modules[mod]
# Should still work with builtin formatter
from metricengine.factories import money
from metricengine.policy import DisplayPolicy
amount = money(1234.56)
policy = DisplayPolicy(currency="USD")
result = amount.format(policy)
# Should contain currency and amount
assert "USD" in result
assert "1234.56" in result or "1,234.56" in result
Performance Tips
Cache Display Policies: Create policies once and reuse them
Cache Formatter Instances: The formatter can be reused across requests
Batch Formatting: Format multiple values with the same policy together
Lazy Loading: Only import Babel when actually needed
# Good: Cache policies
class CachedLocalizer:
def __init__(self):
self._policies = {}
self._formatter = None
@property
def formatter(self):
if self._formatter is None:
self._formatter = get_formatter()
return self._formatter
def get_policy(self, locale: str) -> DisplayPolicy:
if locale not in self._policies:
self._policies[locale] = DisplayPolicy(locale=locale)
return self._policies[locale]
Common Pitfalls
Don’t assume Babel is available - Always handle the fallback case
Test with real locale data - Some locales have surprising formatting rules
Handle currency edge cases - Not all currencies use 2 decimal places
Consider RTL languages - Some locales read right-to-left
Validate locale codes - Invalid locales should fall back gracefully
This guide should get you started with internationalizing your Metric Engine application. The key is to use display policies consistently and test across your target locales.