Skip to main content

per-tenant-locales

"""Per-Tenant Locales and Translation Overrides

This guide explains how tenants can customize their language preferences 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