Skip to content

Odoo Internationalization (i18n) and Translation: Complete Guide

DeployMonkey Team · March 23, 2026 11 min read

How Odoo Translations Work

Odoo uses GNU gettext-style translations. Translatable strings are marked in Python and XML, extracted to PO files, translated, and loaded into the database. At runtime, Odoo serves the translated version based on the user's language preference.

Marking Strings for Translation

Python: The _() Function

from odoo import _, models, fields

class SaleOrder(models.Model):
    _inherit = 'sale.order'

    def action_confirm(self):
        if not self.order_line:
            raise UserError(_('You cannot confirm an order without lines.'))
        self.message_post(body=_('Order confirmed by %s.', self.env.user.name))

Rules for _():

  • Only use with string literals — _('Fixed text') works, _(variable) does not
  • Supports printf-style formatting — _('Hello %s', name)
  • Must import _ from odoo module
  • The extraction tool finds _() calls and adds them to PO files

Field Labels and Help Text

name = fields.Char(string='Order Reference', help='Unique reference for this order')
# 'Order Reference' and the help text are automatically translatable

Field string, help, and selection labels are extracted automatically — no _() needed.

Selection Fields

state = fields.Selection([
    ('draft', 'Draft'),
    ('confirmed', 'Confirmed'),
    ('done', 'Done'),
], string='Status')
# 'Draft', 'Confirmed', 'Done' are all translatable

XML Views

Text content in XML views is automatically translatable:

<form>
    <header>
        <button string="Confirm" name="action_confirm"/>
        <!-- "Confirm" is translatable -->
    </header>
    <sheet>
        <group string="Order Details">
            <!-- "Order Details" is translatable -->
        </group>
    </sheet>
</form>

QWeb Templates

<template id="my_template">
    <p>Welcome to our store</p>
    <!-- Text nodes are automatically translatable -->

    <!-- Dynamic text is NOT translatable -->
    <span t-esc="record.name"/>

    <!-- Use t-attf with translation -->
    <a t-attf-href="/shop">Visit Shop</a>
</template>

PO File Structure

Translation files live in i18n/ directory of your module:

my_module/
  i18n/
    my_module.pot     # Template (source strings)
    fr.po             # French translations
    es.po             # Spanish translations
    de.po             # German translations

PO file format:

# French translation for my_module
msgid "You cannot confirm an order without lines."
msgstr "Vous ne pouvez pas confirmer une commande sans lignes."

msgid "Order confirmed by %s."
msgstr "Commande confirmee par %s."

msgid "Order Reference"
msgstr "Reference de commande"

Exporting Translations

Generate the POT file or export existing translations:

  1. Go to Settings > Translations > Export Translation
  2. Select your module and language
  3. Choose PO File format
  4. Download and save to i18n/ directory

Or via command line:

odoo -d mydb --modules=my_module --i18n-export=my_module/i18n/my_module.pot

Importing Translations

# Import a PO file
odoo -d mydb --i18n-import=my_module/i18n/fr.po --language=fr_FR

# Or via Settings > Translations > Import Translation in the UI

Translatable Fields (translate=True)

Model fields that should store different values per language:

class Product(models.Model):
    _name = 'product.template'

    name = fields.Char(translate=True)  # different name per language
    description = fields.Text(translate=True)
    website_description = fields.Html(translate=True)

When translate=True is set, Odoo stores separate values for each installed language. Users editing the record in French see and modify the French version.

Website Multilingual Setup

  1. Install desired languages at Settings > Translations > Languages
  2. Go to Website > Configuration > Settings
  3. Enable the languages under Website Info
  4. The language selector appears automatically in the website header
  5. URLs get language prefixes: /fr/shop, /es/shop

Programmatic Language Switching

# Read a field in a specific language
product_fr = product.with_context(lang='fr_FR')
french_name = product_fr.name

# Create record with specific language
product = self.env['product.template'].with_context(lang='es_ES').create({
    'name': 'Producto de Ejemplo',
})

Common i18n Pitfalls

  • Concatenation: Never build translatable strings with + — use _('Hello %s', name) instead of _('Hello ') + name
  • Variables in _(): _(variable) does not work — the extractor needs literal strings
  • f-strings: _(f'Hello {name}') does not work — use _('Hello %s', name)
  • Plural forms: Odoo does not have built-in plural handling — use conditional logic
  • Missing .pot file: If the POT file is missing, translators cannot find strings to translate
  • HTML in translations: Avoid HTML tags in _() strings — they break when translated
  • Date/number formatting: Use format_date() and format_amount() instead of manual formatting

Testing Translations

class TestTranslation(TransactionCase):

    def test_french_label(self):
        # Install French
        self.env['res.lang']._activate_lang('fr_FR')
        # Check translated field
        product = self.env['product.template'].with_context(lang='fr_FR').browse(product_id)
        self.assertEqual(product.name, 'Expected French Name')

Best Practices

  • Always wrap user-facing strings in _()
  • Use printf-style formatting: _('Order %s confirmed', order.name)
  • Set translate=True on fields that need per-language values
  • Generate and commit the POT file with your module
  • Export PO files after adding new strings
  • Test with at least one non-English language
  • Use format_date() and format_amount() for locale-aware formatting