GuideMarch 13, 2026

Multi-Language Website in Odoo 19:
Translate Content, URLs & SEO for Global Reach

INTRODUCTION

Your English-Only Website Is Invisible to 75% of the Internet

Only 25.9% of internet users browse in English. If your Odoo website serves a single language, you are actively excluding three-quarters of potential customers. Worse, Google treats untranslated content as irrelevant in non-English markets — your pages won't rank in local SERPs regardless of how good your SEO is.

But "installing a language pack" is not the same as running a multi-language website. A proper implementation requires: translated page content (not just UI strings), localized URL slugs (so /products becomes /fr/produits), hreflang tags (so Google knows which version to serve in each country), a language selector that preserves the current page, and RTL layout support for Arabic, Hebrew, and other right-to-left languages.

This guide walks through every step of building a production-grade multi-language Odoo 19 website — from initial language activation through per-locale SEO optimization.

01

Activating Languages and Installing Translation Packs in Odoo 19

Odoo 19 ships with translation packs for 50+ languages. Activating a language loads the translated UI strings (menus, buttons, labels) but does not translate your website content. That's a separate step we'll cover next. First, let's get the languages installed correctly.

Settings — General Settings > Languages
# Step-by-step in the Odoo backend:
#
# 1. Navigate to Settings > General Settings
# 2. Scroll to "Languages" section
# 3. Click "Manage Languages"
# 4. Click "Activate" next to each language you need
# 5. Odoo downloads and installs the translation pack automatically
#
# For scripted/bulk activation via XML-RPC:

import xmlrpc.client

url = 'https://erp.yourcompany.com'
db = 'production'
uid = 2  # Admin user
password = 'your-api-key'

models = xmlrpc.client.ServerProxy(f'{{url}}/xmlrpc/2/object')

# Languages to activate: [code, name]
languages = [
    ('fr_FR', 'French'),
    ('es_ES', 'Spanish'),
    ('de_DE', 'German'),
    ('ar_001', 'Arabic'),
    ('zh_CN', 'Chinese (Simplified)'),
    ('ja_JP', 'Japanese'),
]

for code, name in languages:
    # Check if already installed
    lang_ids = models.execute_kw(db, uid, password,
        'res.lang', 'search', [[('code', '=', code)]])

    if not lang_ids:
        models.execute_kw(db, uid, password,
            'base.language.install', 'create',
            [{'overwrite': True, 'lang_ids': [(0, 0, {'lang': code})]}])
        print(f'Activated: {{name}}')
    else:
        print(f'Already active: {{name}}')

Assign Languages to Your Website

Activating a language system-wide does not make it available on the website. You must explicitly assign each language to the website object:

Settings — Website > Configuration > Settings
# In the Odoo backend:
#
# 1. Navigate to Website > Configuration > Settings
# 2. Under "Website Info", find the "Languages" field
# 3. Add each language you want visitors to see
# 4. Set the "Default Language" (the fallback when no prefix is in the URL)
#
# The default language URL has NO prefix:
#   English (default): yourcompany.com/products
#   French:            yourcompany.com/fr/products
#   Spanish:           yourcompany.com/es/products
#
# Important: The default language should match your largest audience.
# Changing it later will break existing URLs and SEO rankings.
Don't Activate Languages You Won't Maintain

Every active language creates a translation obligation. If you activate French but never translate your product pages, French visitors see a jarring mix of French buttons and English content. Google may also flag this as thin content — pages that exist but add no value in that language. Only activate languages you commit to translating.

02

Translating Website Content: Pages, Products, and Snippets in Odoo 19

Odoo 19 supports two translation workflows: inline translation (visual editor) and export/import PO files (for professional translators or agencies). Both have trade-offs.

Method 1: Inline Translation via the Website Editor

The fastest way to translate individual pages. Switch to the target language, enter edit mode, and click any text block to translate it in-place.

Website Editor — Inline Translation Workflow
# Inline translation steps:
#
# 1. Open the website frontend (not /web backend)
# 2. Click the language selector in the header
# 3. Switch to the target language (e.g., French)
# 4. Click "Edit" in the top-right toolbar
# 5. Every translatable text block shows a blue border
# 6. Click a block > type the translated text
# 7. Click "Save" when done
#
# What gets translated:
#   ✓ Page titles and body text
#   ✓ Button labels and CTA text
#   ✓ Image alt text (click image > "Description" field)
#   ✓ Snippet content (banners, feature lists, testimonials)
#   ✓ Menu item labels
#
# What does NOT get translated this way:
#   ✗ URL slugs (handled separately — see Step 03)
#   ✗ SEO meta titles and descriptions (handled in Step 04)
#   ✗ Product names/descriptions (use the backend for these)
#   ✗ Dynamic content from controllers

Method 2: Export/Import PO Files for Bulk Translation

For large websites (50+ pages) or when working with professional translators, PO file export is far more efficient than clicking through each page:

Settings — Translations > Export / Import
# Export translatable strings:
#
# 1. Settings > Translations > Export Translation
# 2. Language: French (fr_FR)
# 3. File Format: PO File
# 4. Apps: Select "website" and any custom modules
# 5. Click "Export" — downloads a .po file
#
# The PO file contains entries like:
#
# #. module: website
# #: model:ir.ui.view,arch_db:website.homepage
# msgid "Welcome to Our Company"
# msgstr ""
#
# Fill in msgstr with translations:
# msgstr "Bienvenue dans Notre Entreprise"
#
# Import the translated file:
#
# 1. Settings > Translations > Import Translation
# 2. Language: French (fr_FR)
# 3. Upload the translated .po file
# 4. Check "Overwrite Existing Terms" if updating
# 5. Click "Import"

# ── CLI alternative for automation ──
# Export from the Odoo shell:
odoo-bin --modules=website --language=fr_FR \
    --i18n-export=/tmp/website_fr.po \
    -d production --stop-after-init

# Import translated file:
odoo-bin --language=fr_FR \
    --i18n-import=/tmp/website_fr_translated.po \
    --i18n-overwrite \
    -d production --stop-after-init

Translating Product Data

Product names, descriptions, and attribute values live in the backend, not the website editor. Translate them via the record's translation button:

Backend — Product Translation
# For each product:
#
# 1. Open the product form (Sales > Products or Inventory > Products)
# 2. Hover over any translatable field (Name, Description, etc.)
# 3. Click the globe icon (🌐) that appears
# 4. A popup shows all active languages
# 5. Enter the translation for each language
# 6. Save
#
# Translatable product fields:
#   - Product Name
#   - Sales Description
#   - Website Description (HTML)
#   - Attribute Names (Size, Color, etc.)
#   - Attribute Values (Small, Medium, Large)
#   - Category Names
#
# Bulk product translation via Python:
product = env['product.template'].browse(42)

# Write French translation for the name field
product.with_context(lang='fr_FR').write({
    'name': 'Chaise de Bureau Ergonomique',
    'description_sale': 'Design moderne avec support lombaire réglable.',
    'website_description': '<p>Notre chaise la plus vendue...</p>',
})
Machine Translation: Fast but Dangerous

Odoo 19 can integrate with translation APIs (Google Translate, DeepL) via community modules. These are useful for a first draft, but never publish machine translations without human review. A mistranslated product description or legal page can damage trust faster than having no translation at all. Budget for professional review of at least your homepage, product pages, and checkout flow.

03

Localized URL Slugs: Why /fr/produits Outranks /fr/products Every Time

By default, Odoo 19 adds a language prefix to URLs but keeps the slug in the original language: /fr/products instead of /fr/produits. This is a missed SEO opportunity. Google uses URL keywords as a ranking signal, and French users searching for "produits" are more likely to click a URL containing that word.

Website — Translating URL Slugs Per Language
# Translating page slugs in the Website editor:
#
# 1. Switch to the target language (e.g., French)
# 2. Navigate to the page you want to translate
# 3. Click "Page" in the top toolbar > "Page Properties"
# 4. Edit the "URL" field:
#      Before: /products
#      After:  /produits
# 5. Save
#
# URL mapping result:
#   English (default): yourcompany.com/products
#   French:            yourcompany.com/fr/produits
#   Spanish:           yourcompany.com/es/productos
#   German:            yourcompany.com/de/produkte
#   Arabic:            yourcompany.com/ar/المنتجات
#
# For product detail pages, the slug comes from the product name:
#   English: /shop/ergonomic-office-chair-42
#   French:  /fr/boutique/chaise-de-bureau-ergonomique-42
#   Spanish: /es/tienda/silla-de-oficina-ergonomica-42
#
# The numeric ID suffix (-42) stays the same across languages.
# Odoo uses it for internal routing; the text slug is for SEO only.

Translating Menu Item URLs

Menu items have their own URL field. When you translate a page slug, you must also update the menu item that links to it — otherwise the menu points to a 404:

Website — Menu Translation
# Translating website menu items:
#
# 1. Switch to the target language
# 2. Click "Page" > "Edit Menu"
# 3. For each menu item:
#    a. Edit the label: "Products" → "Produits"
#    b. Edit the URL: "/products" → "/produits"
# 4. Save
#
# Alternatively, translate via the backend:
# Website > Configuration > Menus
# Use the globe icon on the "Name" and "URL" fields
#
# ── Common slug translations (EN → FR / ES / DE) ──
# /shop     → /boutique  /tienda        /laden
# /about-us → /a-propos  /sobre-nosotros /ueber-uns
# /contact  → /contactez-nous /contactenos /kontakt
Never Change Default-Language Slugs After Launch

Changing the English slug from /products to /shop after Google has indexed the original URL will cause a ranking drop — even with 301 redirects. Google takes weeks to months to transfer link equity. Translate slugs for new languages, but leave the default language URLs frozen once they are indexed.

04

Hreflang Tags, Meta Titles, and Per-Locale SEO in Odoo 19

Hreflang tags tell search engines which language version of a page to show in which country. Without them, Google might serve your French page to German users, or index only one language version while ignoring the rest. Odoo 19 generates hreflang tags automatically — but only if your setup is correct.

HTML — Hreflang Output Generated by Odoo 19
<!-- Odoo 19 auto-generates these in the <head> section -->
<!-- when multiple languages are active on the website    -->

<link rel="alternate" hreflang="en"    href="https://yourcompany.com/products" />
<link rel="alternate" hreflang="fr"    href="https://yourcompany.com/fr/produits" />
<link rel="alternate" hreflang="es"    href="https://yourcompany.com/es/productos" />
<link rel="alternate" hreflang="de"    href="https://yourcompany.com/de/produkte" />
<link rel="alternate" hreflang="ar"    href="https://yourcompany.com/ar/المنتجات" />
<link rel="alternate" hreflang="x-default" href="https://yourcompany.com/products" />

<!-- The x-default tag points to your default language. -->
<!-- Google serves this to users whose language isn't   -->
<!-- covered by any other hreflang tag.                 -->

<!-- REQUIREMENTS for correct hreflang generation:       -->
<!--   1. All languages assigned to the website object   -->
<!--   2. Pages published in ALL active languages        -->
<!--   3. Canonical URLs set correctly (no duplicates)   -->
<!--   4. URL prefix matches the language code           -->

Translating SEO Meta Titles and Descriptions

Hreflang tells Google which page to show. The meta title and description determine whether users click. A French user seeing an English meta description in their search results will skip your listing — even if the page content is fully translated.

Website — Per-Language SEO Meta Tags
# Translating meta titles and descriptions:
#
# 1. Switch to the target language in the website frontend
# 2. Navigate to the page
# 3. Click "Page" > "Page Properties" > "SEO" tab
# 4. Translate:
#    - Page Title: "Ergonomic Office Chairs" → "Chaises de Bureau Ergonomiques"
#    - Meta Description: translate the full snippet
#    - Meta Keywords: use localized keywords
# 5. Save
#
# Example per-language meta for a product category page:
#
# EN:
#   Title:       Ergonomic Office Chairs | YourCompany
#   Description: Shop our range of ergonomic office chairs.
#                Free shipping on orders over $50.
#
# FR:
#   Title:       Chaises de Bureau Ergonomiques | YourCompany
#   Description: Découvrez notre gamme de chaises de bureau
#                ergonomiques. Livraison gratuite dès 50€.
#
# Repeat for each active language (ES, DE, AR, etc.)

Structured Data and Open Graph Per Language

Odoo 19 renders Open Graph tags (og:title, og:description) from the same translated fields used for SEO meta. If your meta title is translated, the OG tags follow automatically. However, structured data (JSON-LD for products, breadcrumbs, organization) requires custom attention:

QWeb — Localized JSON-LD Structured Data
<!-- Add to your website layout template or page-specific template -->
<template id="website_sale_json_ld_localized"
          inherit_id="website_sale.product">
  <xpath expr="//div[@id='product_detail']" position="before">
    <script type="application/ld+json">
    {
      "@context": "https://schema.org/",
      "@type": "Product",
      "name": "<t t-esc="product.name"/>",
      "description": "<t t-esc="product.description_sale"/>",
      "url": "<t t-esc="request.httprequest.url"/>",
      "inLanguage": "<t t-esc="request.env.lang"/>",
      "offers": {
        "@type": "Offer",
        "priceCurrency": "<t t-esc="website.currency_id.name"/>",
        "price": "<t t-esc="product.list_price"/>",
        "availability": "https://schema.org/InStock"
      }
    }
    </script>
  </xpath>
</template>
Validate Hreflang with Google Search Console

After deploying your multi-language site, go to Google Search Console > International Targeting > Language. Google reports hreflang errors here: missing return tags, inconsistent URLs, and languages with no indexed pages. Fix these within the first week — hreflang errors compound over time and can take months to recover from once Google de-indexes a language variant.

05

Building a Language Selector That Preserves Context and Doesn't Break UX

Odoo 19 includes a default language selector widget. It works, but has UX problems: it redirects to the homepage instead of the equivalent page, it shows language codes instead of names, and it doesn't indicate the current language clearly. Here's how to build a better one.

QWeb — Custom Language Selector Template
<!-- Inherit the website layout to add a custom language selector -->
<template id="custom_language_selector"
          inherit_id="website.layout"
          name="Custom Language Selector">
  <xpath expr="//header" position="inside">
    <div class="language-selector dropdown">
      <button class="btn btn-link dropdown-toggle"
              data-bs-toggle="dropdown"
              aria-expanded="false">
        <!-- Show flag + language name -->
        <span t-esc="request.env.lang[:2].upper()"/>
        <span t-esc="request.env['res.lang']._lang_get(
              request.env.lang).name"/>
      </button>
      <ul class="dropdown-menu">
        <t t-foreach="request.website.language_ids" t-as="lg">
          <li t-att-class="'active' if lg.code == request.env.lang
                           else ''">
            <a class="dropdown-item"
               t-att-href="url_for(request.httprequest.path,
                           lang_code=lg.url_code)"
               t-att-hreflang="lg.url_code">
              <span t-esc="lg.url_code[:2].upper()"/>
              <span t-esc="lg.name"/>
            </a>
          </li>
        </t>
      </ul>
    </div>
  </xpath>
</template>

<!-- Key behavior:
     url_for() translates the CURRENT page URL to the target language.
     This preserves context: switching from /fr/produits to German
     lands on /de/produkte, NOT the German homepage.

     The hreflang attribute on each link helps crawlers discover
     all language variants from the selector itself.
-->

Geo-Based Language Detection

Automatically redirecting users based on their IP geolocation is tempting but risky. Google explicitly warns against geo-based redirects because Googlebot crawls from the US — if you redirect US IPs to English, Google never sees your French pages. Instead, use a suggestion banner:

JavaScript — Non-Blocking Language Suggestion Banner
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";

publicWidget.registry.LanguageSuggestion = publicWidget.Widget.extend({
    selector: '#wrapwrap',

    start() {
        this._super(...arguments);
        this._suggestLanguage();
    },

    _suggestLanguage() {
        // Get browser language (e.g., "fr-CA" → "fr")
        const browserLang = navigator.language?.slice(0, 2);
        const currentLang = document.documentElement.lang?.slice(0, 2);

        // Don't show if already matching or if user dismissed
        if (browserLang === currentLang) return;
        if (sessionStorage.getItem('lang_suggestion_dismissed')) return;

        // Check if we support this language
        const supported = document.querySelectorAll(
            'link[rel="alternate"][hreflang]'
        );
        let targetUrl = null;
        supported.forEach(link => {
            if (link.hreflang?.startsWith(browserLang)) {
                targetUrl = link.href;
            }
        });

        if (!targetUrl) return;

        // Show a non-blocking banner (NOT a redirect)
        const banner = document.createElement('div');
        banner.className = 'lang-suggestion-banner';
        banner.innerHTML = `
            <span>This page is available in your language.</span>
            <a href="${{targetUrl}}">Switch</a>
            <button class="dismiss">×</button>
        `;
        banner.querySelector('.dismiss').addEventListener('click', () => {
            banner.remove();
            sessionStorage.setItem('lang_suggestion_dismissed', '1');
        });
        document.body.prepend(banner);
    },
});
Respect the Accept-Language Header

Odoo 19's website module reads the browser's Accept-Language header on the first visit and sets a cookie with the best matching language. This happens server-side and works with Googlebot (which sends Accept-Language: en). The banner approach above is a client-side supplement — it catches cases where the cookie logic doesn't apply, like direct links from social media.

06

RTL Layout Support for Arabic, Hebrew, and Persian in Odoo 19

Right-to-left (RTL) languages aren't just mirrored text — the entire layout must flip. Navigation moves to the right, sidebars swap sides, progress bars fill from right to left, and CSS properties like margin-left become margin-right. Odoo 19 handles RTL better than previous versions, but custom themes and snippets almost always break.

SCSS — RTL-Aware Custom Theme Styles
// Odoo 19 adds dir="rtl" to the <html> element for RTL languages.
// Use this attribute selector to write RTL-specific overrides.

// ── Base approach: Use logical properties ──
// Instead of left/right, use start/end (works for both LTR and RTL)

.product-card {
  // ✓ Good: logical properties (auto-flip for RTL)
  margin-inline-start: 1rem;
  padding-inline-end: 0.5rem;
  border-inline-start: 3px solid #21B890;
  text-align: start;

  // ✗ Bad: physical properties (break in RTL)
  // margin-left: 1rem;
  // padding-right: 0.5rem;
  // border-left: 3px solid #21B890;
  // text-align: left;
}

// ── When you must use physical properties ──
// Some cases (transforms, background positions) need explicit RTL rules

.hero-banner {
  background-position: right center;

  [dir="rtl"] & {
    background-position: left center;
  }
}

.slide-in-animation {
  transform: translateX(-100%);
  animation: slideIn 0.3s forwards;
  [dir="rtl"] & {
    transform: translateX(100%);
  }
}
@keyframes slideIn { to { transform: translateX(0); } }

// ── Flexbox: auto-reverses with dir="rtl" ──
// Do NOT use row-reverse as a "hack" for RTL — it breaks LTR.
.checkout-steps {
  display: flex;
  gap: 1rem;
}

// ── Typography: RTL-specific font stack ──
[dir="rtl"] body {
  font-family: 'Noto Sans Arabic', 'Noto Sans Hebrew', sans-serif;
  // Arabic text needs slightly more line-height
  line-height: 1.8;
}

// ── Icon arrows: flip for RTL ──
.breadcrumb-separator,
.pagination-arrow {
  [dir="rtl"] & { transform: scaleX(-1); }
}

Testing RTL Without Native Speakers

You don't need to read Arabic to verify RTL layout. Use the browser DevTools to force RTL and check for visual regressions:

Browser Console — Force RTL for Testing
// Paste in browser console to toggle RTL layout:
document.documentElement.dir = 'rtl';
document.documentElement.lang = 'ar';

// Revert to LTR:
document.documentElement.dir = 'ltr';
document.documentElement.lang = 'en';

// RTL visual checklist:
// □ Header nav: flows right-to-left
// □ Sidebar: appears on the right side
// □ Text alignment: right-aligned
// □ Breadcrumb/pagination arrows: flipped
// □ Number inputs: digits remain LTR (universal)
// □ Carousel: previous/next swapped
// □ Checkout steps: progress right-to-left
Numbers and Embedded LTR Content

Arabic text containing numbers, URLs, or English brand names must use bidirectional isolation. Wrap embedded LTR content in <bdi> tags or apply unicode-bidi: isolate in CSS. Without this, a price like "50 USD" can render as "USD 50" or worse, "05 USD" when surrounded by RTL text. Odoo 19 handles this in most built-in templates, but custom snippets need manual attention.

07

4 Multi-Language Mistakes That Tank Your International SEO

1

Publishing Pages Before Translation Is Complete

When you activate a new language, every published page instantly becomes "available" in that language — serving the untranslated English content with a French URL prefix. Google indexes these pages and flags them as duplicate content across language variants. Your domain's crawl budget gets wasted on identical pages, and both the English and French versions may drop in rankings.

Our Fix

Before activating a language on the website, complete translations for at least your top 20 pages (homepage, key product categories, about, contact). For remaining pages, use the website_published field per-language to unpublish untranslated content: page.with_context(lang='fr_FR').write({'website_published': False}).

2

Forgetting to Translate URL Slugs and Meta Descriptions

Translating page content but leaving URLs as /fr/products and meta descriptions in English creates a disjointed experience. French users see a French page but an English URL in the address bar and English text in Google search results. Click-through rates drop by 30-50% when the SERP snippet language doesn't match the user's search query language.

Our Fix

Add URL slug and meta description translation to your translation checklist. Create a spreadsheet with columns for each language: Page Name, URL Slug, Meta Title, Meta Description. Hand this to your translator alongside the PO files. Don't treat SEO metadata as an afterthought.

3

Using Country Flags as Language Icons

A French flag for the French language excludes French-speaking users in Belgium, Switzerland, Canada, and 25 African countries. A US flag for English excludes the UK, Australia, India, and dozens more. Flags represent countries, not languages. Using them in your language selector alienates the very international audience you're trying to reach.

Our Fix

Use the language name written in that language: "Francais" (not "French"), "Deutsch" (not "German"), "العربية" (not "Arabic"). If you need a visual indicator, use a globe icon or the two-letter language code (FR, DE, AR). Never flags.

4

Missing x-default Hreflang Tag

Odoo 19 generates hreflang tags for each active language, but some configurations miss the x-default tag. This tag tells Google which version to show when the user's language doesn't match any available version. Without it, Google picks one arbitrarily — and it might pick Arabic for your US visitors.

Our Fix

Verify the x-default tag exists in your page source. It should point to your default language URL (without a language prefix). If missing, add it via a custom QWeb template that inherits website.layout and injects the tag into the <head> section.

BUSINESS ROI

The Revenue Impact of a Properly Localized Odoo Website

Multi-language isn't a "nice feature" — it's a revenue multiplier for any business selling outside its home market:

72%Buy in Native Language

72.4% of consumers are more likely to buy a product with information in their own language (CSA Research). This applies to B2B buyers too — decision-makers prefer vendors who speak their language.

3xOrganic Traffic

Each language variant creates a new set of indexable pages targeting local keywords. A 5-language site has 5x the keyword surface area. Clients typically see 2-3x organic traffic within 6 months of launching localized content.

40%Lower Bounce Rate

Pages served in the visitor's language see 30-40% lower bounce rates. Users stay longer, browse more products, and are significantly more likely to complete checkout or submit a contact form.

The math is simple: if your website generates $100K/month in English and you properly localize for French, Spanish, and German markets, you can realistically expect $40-80K in additional monthly revenue from those markets within the first year — assuming you have the product-market fit and fulfillment capability.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to building a multi-language Odoo 19 website. Covers language activation, content translation, localized URL slugs, hreflang tags, RTL support, and per-locale SEO.

H2 Keywords

1. "Activating Languages and Installing Translation Packs in Odoo 19"
2. "Translating Website Content: Pages, Products, and Snippets in Odoo 19"
3. "Localized URL Slugs: Why /fr/produits Outranks /fr/products Every Time"
4. "Hreflang Tags, Meta Titles, and Per-Locale SEO in Odoo 19"
5. "RTL Layout Support for Arabic, Hebrew, and Persian in Odoo 19"

Your Competitors Already Speak Your Customers' Language

A multi-language website is not a luxury — it's the minimum expectation for any business operating across borders. Every day your website serves only English, you're losing qualified leads to competitors who bothered to translate. Odoo 19 gives you the technical foundation; this guide gives you the implementation playbook.

If you need help planning your multi-language rollout, we can audit your current Odoo website and deliver a localization roadmap. We assess your target markets, recommend which languages to prioritize, estimate translation scope, and configure everything from hreflang tags to RTL stylesheets. Most projects go from monolingual to fully localized in 4-6 weeks.

Get a Free Localization Audit