How to Define Translations in Custom Modules & Add Languages in Odoo 18
By Braincuber Team
Published on January 17, 2026
Odoo's built-in modules ship with translations for dozens of languages. But when you build a custom module, those string translations don't exist—you have to create them yourself. Whether you're deploying to a French warehouse, a German sales team, or a Spanish service center, internationalization (i18n) is essential for user adoption.
This tutorial covers the complete translation workflow in Odoo 18: activating languages, exporting translation templates, creating PO files, and even adding entirely new languages that Odoo doesn't support out of the box. By the end, your custom module will speak as many languages as your business needs.
How Odoo Translations Work: Translatable strings are marked in Python with _("string") and in XML with translate="1". Odoo extracts these into PO files, where you provide translations. When a user switches languages, Odoo looks up the translated strings.
Why Translations Matter
Global Reach
Serve users in their native language. A French warehouse worker and a German accountant can use the same Odoo instance, each in their preferred language.
User Adoption
Users are more productive and make fewer errors when working in their native language. Translations reduce training time significantly.
Compliance
Some regions require software interfaces to be available in the local language. Proper i18n ensures you meet these requirements.
Professional Quality
Untranslated strings look unfinished. Complete translations signal a polished, production-ready module.
Step 1: Activate Languages in Odoo
Before translating, activate the target languages in your database:
Access Language Settings
- Go to Settings → General Settings
- Scroll to Languages section
- Click Manage Languages or go to Settings → Translations → Languages
Activate Target Languages
- Find your target language (e.g., French, German, Spanish)
- Click on the language to open its settings
- Enable Active checkbox
- Odoo will load base translations for that language
Step 2: Make Strings Translatable
In your custom module, mark strings for translation:
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class InventoryRequest(models.Model):
_name = 'inventory.request'
_description = _('Inventory Request') # Translatable
name = fields.Char(string=_('Request Number'), required=True)
state = fields.Selection([
('draft', _('Draft')), # Translatable selection
('pending', _('Pending Approval')),
('approved', _('Approved')),
('rejected', _('Rejected')),
], default='draft', string=_('Status'))
notes = fields.Text(string=_('Internal Notes'))
def action_submit(self):
"""Submit request for approval"""
if not self.product_lines:
raise ValidationError(_('Please add at least one product line.'))
self.state = 'pending'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Request Submitted'), # Translatable notification
'message': _('Your request has been submitted for approval.'),
'type': 'success',
}
}
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_inventory_request_form" model="ir.ui.view">
<field name="name">inventory.request.form</field>
<field name="model">inventory.request</field>
<field name="arch" type="xml">
<form>
<header>
<!-- Button labels auto-translate via string attribute -->
<button name="action_submit" type="object"
string="Submit for Approval" class="btn-primary"
invisible="state != 'draft'"/>
</header>
<sheet>
<!-- Group labels translate via string attribute -->
<group string="Request Details">
<field name="name"/>
<field name="state"/>
</group>
<!-- Labels in notebook pages -->
<notebook>
<page string="Products" name="products">
<field name="product_lines"/>
</page>
<page string="Notes" name="notes">
<field name="notes" placeholder="Add internal notes..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>
Step 3: Export Translation Template
Odoo can extract all translatable strings into a PO file template:
Open Export Dialog
- Go to Settings → Translations → Export Translation
- Select Language: Target language (e.g., French / Français)
- Select File Format: PO File (recommended)
- Select Apps to Export: Your custom module
- Click Export
Download the PO File
Odoo generates a file like fr.po containing all extractable strings. Download it for editing.
Step 4: Create Translations
Edit the PO file to add translations. Place it in your module's i18n folder:
inventory_request/ ├── models/ │ └── inventory_request.py ├── views/ │ └── inventory_request_views.xml ├── i18n/ │ ├── fr.po # French translations │ ├── de.po # German translations │ └── es.po # Spanish translations ├── __init__.py └── __manifest__.py
# Translation of Odoo Server. # This file contains the translation of the following modules: # inventory_request # msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: fr\n" #. module: inventory_request #: model:ir.model,name:inventory_request.model_inventory_request msgid "Inventory Request" msgstr "Demande d'inventaire" #. module: inventory_request #: model:ir.model.fields,field_description:inventory_request.field_inventory_request__name msgid "Request Number" msgstr "Numéro de demande" #. module: inventory_request #: model:ir.model.fields.selection,name:inventory_request.selection__inventory_request__state__draft msgid "Draft" msgstr "Brouillon" #. module: inventory_request #: model:ir.model.fields.selection,name:inventory_request.selection__inventory_request__state__pending msgid "Pending Approval" msgstr "En attente d'approbation" #. module: inventory_request #: code:addons/inventory_request/models/inventory_request.py:0 msgid "Please add at least one product line." msgstr "Veuillez ajouter au moins une ligne de produit." #. module: inventory_request #: code:addons/inventory_request/models/inventory_request.py:0 msgid "Request Submitted" msgstr "Demande soumise"
Important: The file name must match the language code exactly: fr.po for French, de.po for German, es.po for Spanish. Upgrade your module after adding translations.
Step 5: Import Translations
For updating existing translations or importing externally created PO files:
- Go to Settings → Translations → Import Translation
- Upload your PO file
- Select the target Language
- Enable Overwrite Existing Terms if updating
- Click Import
Adding a New Language to Odoo
If your target language isn't in Odoo's language list, you can add it via CSV:
"id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","week_start" "base.lang_sw","Swahili / Kiswahili","sw","sw","ltr","[]",".",",","%d/%m/%Y","%H:%M:%S","1"
{
'name': 'Inventory Request',
'version': '18.0.1.0.0',
'depends': ['base', 'stock'],
'data': [
'security/ir.model.access.csv',
'data/res.lang.csv', # Add new language
'views/inventory_request_views.xml',
],
'installable': True,
'license': 'LGPL-3',
}
Best Practices
Translation Tips:
- Use _() consistently: Wrap all user-facing strings in Python with
_("string"). - Keep context: PO files include source locations—use them to understand context when translating.
- Test with real users: Have native speakers review translations for accuracy and natural phrasing.
- Update after code changes: Re-export PO files after adding new strings to ensure nothing is missed.
Conclusion
Internationalization in Odoo 18 is a straightforward process: mark strings with _(), export a PO template, add translations, and place the file in i18n/. For languages not in Odoo's default list, you can add them via CSV data files. With proper translations, your custom module can serve users worldwide in their native language.
Key Takeaway: Activate target language → Mark strings with _() in Python and string attribute in XML → Export PO template from Settings → Add translations → Place in i18n/[lang_code].po → Upgrade module.
