The Global Launch Problem
Your D2C brand launches globally. Day 1: English-only. Day 30: French customers complain they see English menus. Day 60: Spanish team can't navigate the system. Day 90: You're paying $18,000 for emergency translation.
Get i18n Wrong
• Hard-coded strings in Python code
• Labels stuck in English in every UI
• No way to support new languages without re-coding
• Each language needs custom module rebuild
Get i18n Right
✓ Strings marked for translation automatically
✓ New languages added by translating PO files
✓ Users see everything in their language
✓ Community can contribute translations
We've implemented 150+ Odoo systems. The ones where developers built i18n from day one? They serve 20+ languages. Translating is just a file upload. The ones who didn't plan for i18n? They're stuck with English-only modules that can't be translated without developer work. That's $30,000-$80,000 in emergency retrofitting and consulting.
Part 1: Marking Strings for Translation
What it is: Using the _() function to mark strings that should be translated.
In Python Code
from odoo import models, fields, _
class SaleOrder(models.Model):
_name = 'sale.order'
_description = _('Sale Order') # Translatable description
# Field labels with translations
customer_id = fields.Many2one(
'res.partner',
string=_('Customer'), # Mark as translatable
help=_('Select the customer for this order')
)
status = fields.Selection(
selection=[
('draft', _('Draft')),
('sent', _('Quotation Sent')),
('sale', _('Order Confirmed')),
('done', _('Done')),
('cancel', _('Cancelled')),
],
string=_('Status'),
default='draft'
)
# Error messages (translatable)
def action_confirm(self):
if not self.customer_id:
raise ValidationError(_('Customer is required to confirm order'))
self.state = 'sale'
In XML Views
<record id="view_order_form" model="ir.ui.view">
<field name="name">Sale Order</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<form>
<sheet>
<!-- Field labels auto-translatable in views -->
<field name="customer_id"/>
<!-- String attribute (translatable) -->
<separator string="Order Details"/>
<!-- Button labels (translatable) -->
<button name="action_confirm" type="object"
string="Confirm Order" class="btn-primary"/>
</sheet>
</form>
</field>
</record>
Key Rule:
Every user-facing string must use _().
Common Mistakes
# ❌ WRONG - Not translatable
def action_confirm(self):
raise ValidationError('Customer is required') # Hard-coded, won't translate
# ✅ RIGHT - Translatable
def action_confirm(self):
raise ValidationError(_('Customer is required'))
Part 2: Creating Translation Files (PO Files)
What they are: Text files containing translations for each language.
Module Structure
custom_module/
├── models/
├── views/
├── i18n/ # Translation folder
│ ├── custom_module.pot # Translation template (source strings)
│ ├── fr_FR.po # French translations
│ ├── es_ES.po # Spanish translations
│ ├── de_DE.po # German translations
│ └── ar_AR.po # Arabic translations
└── __manifest__.py
Generate PO Files
Step 1: Export from Odoo
Settings → Translations → Export Translations
Module: custom_module
Language: French
File Format: PO File (.po)
Step 2: PO File Structure
# Translation file for custom_module
#. Field string in model sale.order
#: model:ir.model.fields,string:custom_module.field_sale_order__customer_id
msgid "Customer"
msgstr "Client"
#. Selection value
#: model:ir.model.fields.selection,string:custom_module.selection_sale_order__state__draft
msgid "Draft"
msgstr "Brouillon"
#. Button label
#: model:ir.actions.act_window,name:custom_module.action_sale_order
msgid "Sale Orders"
msgstr "Commandes de Vente"
#. Error message
#: code:addons/custom_module/models/sale_order.py:0
msgid "Customer is required to confirm order"
msgstr "Le client est obligatoire pour confirmer la commande"
Step 3: Translate msgstr Values
msgid "Customer"
msgstr "Cliente" # Spanish
msgid "Sale Orders"
msgstr "Pedidos de Venta" # Spanish
Step 4: Place in i18n Folder
custom_module/i18n/es_ES.po # Spanish
custom_module/i18n/fr_FR.po # French
custom_module/i18n/de_DE.po # German
Step 5: Upgrade Module
Odoo automatically detects .po files and loads translations on module upgrade.
Part 3: Translatable Fields (Dynamic Content)
Problem: Product descriptions change. User changes language. Description should translate too.
Solution: Add translate=True to fields
class Product(models.Model):
_inherit = 'product.product'
# Regular field (not translatable)
name = fields.Char(string='Name')
# Translatable field (changes per language)
description = fields.Text(
string='Description',
translate=True, # KEY: Enables per-language content
help='Product description (translates to each language)'
)
care_instructions = fields.Text(
string='Care Instructions',
translate=True # Each language has different instructions
)
How It Works
| Language | Product Name | Description |
|---|---|---|
| English | T-Shirt (same) | Made from 100% organic cotton |
| French | T-Shirt (same) | Fabriqué à 100% en coton biologique |
| Spanish | T-Shirt (same) | Hecho de 100% algodón orgánico |
Real D2C Example: Complete Translatable Module
from odoo import models, fields, api, _
class ProductTemplate(models.Model):
_inherit = 'product.template'
# Translatable fields
name = fields.Char(
string=_('Product Name'),
translate=True, # Translates to each language
required=True
)
description = fields.Html(
string=_('Description'),
translate=True
)
care_instructions = fields.Text(
string=_('Care Instructions'),
translate=True,
help=_('How to care for this product')
)
size_guide = fields.Html(
string=_('Size Guide'),
translate=True,
help=_('Translatable size guide HTML')
)
# Regular fields (NOT translated)
sku = fields.Char(string=_('SKU'))
barcode = fields.Char(string=_('Barcode'))
list_price = fields.Float(string=_('List Price'))
@api.constrains('name')
def _check_name(self):
for product in self:
if not product.name:
raise ValidationError(_('Product name is required'))
<record id="view_product_form" model="ir.ui.view">
<field name="name">Product Form</field>
<field name="model">product.template</field>
<field name="arch" type="xml">
<form>
<sheet>
<!-- Translatable field -->
<field name="name"/>
<!-- Regular field (not translated) -->
<field name="sku"/>
<!-- Translatable HTML field -->
<field name="description" widget="html"/>
<!-- Translatable multi-line field -->
<field name="care_instructions"/>
</sheet>
</form>
</field>
</record>
Part 4: Language Configuration
Activate Languages
Settings → General Settings → Languages
Or programmatically:
# Activate French
lang = self.env['res.lang'].search([('code', '=', 'fr_FR')])
if lang:
lang.active = True
# Add language if doesn't exist
self.env['res.lang'].create({
'name': 'French',
'code': 'fr_FR',
'iso_code': 'fr',
'active': True,
})
Set User Language
# Set user to use French
user = self.env.user
user.lang = 'fr_FR'
# Now when user accesses system, everything is in French
Context-Based Translation
# Get customer's language
customer = self.env['res.partner'].browse(customer_id)
customer_lang = customer.lang
# Fetch product in customer's language
product = self.env['product.product'].with_context(lang=customer_lang).browse(product_id)
# Now product.description is in customer's language
Part 5: Generate Missing Translation Terms
Problem: You add new translatable strings. They need to appear in PO files.
Solution: Generate missing terms
Settings → Translations → Generate Missing Terms
This scans your module and creates entries in translation files for all new _() marked strings.
Best Practices Checklist
In Python Code
# ✅ DO THIS
from odoo import _, models
class MyModel(models.Model):
name = fields.Char(string=_('Name'))
def action(self):
raise ValidationError(_('Error message'))
# ❌ DON'T DO THIS
class MyModel(models.Model):
name = fields.Char(string='Name') # No _()
def action(self):
raise ValidationError('Error message') # Hard-coded
In Field Definitions
# ✅ DO THIS
description = fields.Text(
string=_('Description'),
translate=True,
help=_('Product description'))
# ❌ DON'T DO THIS
description = fields.Text(string='Description') # No translate=True
Your Action Items
Plan Translations
❏ List all languages your module should support
❏ Identify all user-facing strings
❏ Mark strings with _() in Python
❏ Mark string attributes in XML views
Create Translation Files
❏ Export translation template from Odoo
❏ Generate PO files for each language
❏ Have translators fill in translations
❏ Place PO files in i18n/ folder
Test Translations
❏ Install module
❏ Activate language in Settings
❏ Change user language
❏ Verify all strings translate correctly
Free i18n Implementation Workshop
Stop building English-only modules. We'll plan your translation strategy, mark all translatable strings, generate PO files, configure languages, test with multiple languages, create workflow for managing translations. Most D2C brands need multiple languages but don't plan for it. Retrofitting translations costs $20,000-$50,000. Building i18n from the start is free.
