Skip to main content

Per-Tenant Locales and Translation Overrides

and override system translations to match their specific terminology, industry language, or brand voice.

Overview

Framework M supports tenant-specific localization through two mechanisms:

  1. Tenant Default Locale: Set organization-wide language preference
  2. Tenant Translation Overrides: Customize specific translations

Setting Tenant Default Locale

Backend: Configure Tenant

Set the default locale in the tenant's attributes:

from framework_m.core.interfaces.tenant import TenantContext

# Configure tenant with Hindi as default locale
tenant = TenantContext(
tenant_id="acme-corp",
attributes={
"default_locale": "hi", # Hindi
"plan": "enterprise",
"features": ["advanced_reports"]
}
)

Locale Resolution Priority

When a user makes a request, the locale is resolved in this order:

  1. Accept-Language header (browser preference)
  2. User.locale field (personal preference)
  3. Tenant default_locale (organization default) ← Tenant setting
  4. System DEFAULT_LOCALE (fallback to "en")

Example: Healthcare Organization

# Healthcare tenant defaults to Tamil
healthcare_tenant = TenantContext(
tenant_id="healthcare-india",
attributes={
"default_locale": "ta", # Tamil
"industry": "healthcare"
}
)

# All users in this tenant see Tamil by default
# unless they:
# - Set their browser to a different language
# - Set their personal locale preference

Tenant Translation Overrides

Tenants can provide custom translations to override system translations. This is useful for:

  • Industry-specific terminology
  • Brand-specific wording
  • Localized company names in messages

Creating Tenant Translations

Option 1: Via API

# Create tenant-specific translation
POST /api/v1/TenantTranslation
Authorization: Bearer <token>
X-Tenant-ID: acme-corp

{
"tenant_id": "acme-corp",
"source_text": "Customer",
"translated_text": "Patient",
"locale": "en",
"context": "field_label"
}

Option 2: Via Python Code

from framework_m.core.doctypes.tenant_translation import TenantTranslation

# Healthcare tenant uses "Patient" instead of "Customer"
patient_translation = TenantTranslation(
tenant_id="healthcare-india",
source_text="Customer",
translated_text="Patient",
locale="en",
context="field_label"
)

# Same in Tamil
patient_translation_ta = TenantTranslation(
tenant_id="healthcare-india",
source_text="Customer",
translated_text="நோயாளி", # Patient in Tamil
locale="ta",
context="field_label"
)

Translation Priority

When translating text, the system checks in this order:

  1. TenantTranslation (tenant-specific override)
  2. Translation (system-wide translation)
  3. Default parameter (if provided)
  4. Source text (original text)

Example: Retail vs Healthcare

# System translation (default)
Translation(
source_text="Customer",
translated_text="ग्राहक", # Customer in Hindi
locale="hi",
context="field_label"
)

# Retail tenant override (uses "Client")
TenantTranslation(
tenant_id="retail-corp",
source_text="Customer",
translated_text="क्लाइंट", # Client in Hindi
locale="hi",
context="field_label"
)

# Healthcare tenant override (uses "Patient")
TenantTranslation(
tenant_id="healthcare-india",
source_text="Customer",
translated_text="रोगी", # Patient in Hindi
locale="hi",
context="field_label"
)

Result

When each tenant's users see the "Customer" field:

  • Retail tenant: "क्लाइंट" (Client)
  • Healthcare tenant: "रोगी" (Patient)
  • Other tenants: "ग्राहक" (Customer - system default)

Managing Tenant Translations

List Tenant Translations

# Get all translations for a tenant
GET /api/v1/TenantTranslation?filters=[{"field":"tenant_id","operator":"eq","value":"acme-corp"}]
Authorization: Bearer <token>
X-Tenant-ID: acme-corp

Update Tenant Translation

# Update existing translation
PUT /api/v1/TenantTranslation/{id}
Authorization: Bearer <token>
X-Tenant-ID: acme-corp

{
"translated_text": "Updated translation"
}

Delete Tenant Translation

# Remove custom translation (falls back to system translation)
DELETE /api/v1/TenantTranslation/{id}
Authorization: Bearer <token>
X-Tenant-ID: acme-corp

Row-Level Security

TenantTranslation DocType has RLS enabled:

class TenantTranslation(BaseDocType):
class Meta:
apply_rls = True
rls_field = "tenant_id"

This ensures:

  • Tenants can only see/modify their own translations
  • System automatically filters by tenant_id
  • No cross-tenant data leakage

Frontend Usage

Automatic Translation

Field labels in forms are automatically translated based on:

  1. User's resolved locale
  2. Tenant overrides (if any)
// Frontend fetches metadata
const meta = await fetch('/api/meta/Invoice', {
headers: {
'X-Tenant-ID': 'healthcare-india',
'Accept-Language': 'ta'
}
});

// Response includes translated field labels
{
"doctype": "Invoice",
"locale": "ta",
"schema": {
"properties": {
"customer": {
"description": "நோயாளி", // "Patient" (tenant override)
"type": "string"
}
}
}
}

Translation Context

Use context to disambiguate translations:

# Button label
TenantTranslation(
tenant_id="acme-corp",
source_text="Save",
translated_text="सुरक्षित करें", # "Secure/Protect" emphasis
locale="hi",
context="button"
)

# Field label
TenantTranslation(
tenant_id="acme-corp",
source_text="Save",
translated_text="बचत", # "Savings" (financial context)
locale="hi",
context="field_label"
)

Best Practices

1. Start with System Translations

Only create tenant overrides when necessary:

# Good: Override for industry-specific term
TenantTranslation(
tenant_id="medical-corp",
source_text="Customer",
translated_text="Patient",
locale="en"
)

# Avoid: Duplicating system translations unnecessarily
# (use system Translation instead)

2. Use Consistent Context

# Consistent context for all field labels
context="field_label"

# Consistent context for all buttons
context="button"

# Consistent context for all messages
context="message"

3. Document Tenant Customizations

Keep a record of why overrides exist:

# Document the reason in comments or separate docs
TenantTranslation(
tenant_id="healthcare-india",
source_text="Total Amount",
translated_text="மொத்த கட்டணம்", # "Total Fee" per healthcare terminology
locale="ta",
context="field_label"
)

4. Test in Multiple Locales

# Test each tenant's locale
curl -H "X-Tenant-ID: healthcare-india" \
-H "Accept-Language: ta" \
/api/meta/Invoice

curl -H "X-Tenant-ID: retail-corp" \
-H "Accept-Language: hi" \
/api/meta/Invoice

Migration from Single-Tenant

If migrating from single-tenant to multi-tenant:

  1. Review existing translations: Decide which are tenant-specific
  2. Create tenant translations: Move tenant-specific translations to TenantTranslation
  3. Keep system translations: Keep common translations in Translation
  4. Test thoroughly: Verify each tenant sees correct translations

See Also

  • framework_m.core.doctypes.tenant_translation - TenantTranslation DocType
  • framework_m.adapters.i18n.DefaultI18nAdapter - Translation adapter with tenant support
  • framework_m.adapters.web.middleware.locale - Locale resolution middleware
  • docs/developer/locale-resolution.md - Complete locale resolution guide