OpenCart Multilingual and Multicurrency Setup
OpenCart is designed from the ground up with multi-language and multi-currency support — this is not an add-on but part of the core. However, "works out of the box" and "configured correctly" are different things. Here we'll cover full setup cycle including SEO-URLs for each language, auto-updated exchange rates, and correct price display for different markets.
Installing Languages
OpenCart ships with English (en-gb). To add Russian, Belarusian, and other languages:
1. Download language pack.
Official languages available at opencart.com or github.com/opencart/opencart. OpenCart 4.x language pack structure:
ru-ru.zip
├── admin/language/ru-ru/
│ ├── common/
│ ├── catalog/
│ ├── customer/
│ └── ...
└── catalog/language/ru-ru/
├── common/
├── product/
├── checkout/
└── ...
2. Install via Extension Installer:
Extensions → Extension Installer → Upload → ru-ru.zip
3. Activate language:
System → Localization → Languages → Add Language
→ Language Name: Russian
→ Code: ru-ru
→ Locale: ru_RU.UTF-8
→ Image: flag/ru.png
→ Directory: ru-ru
→ Sort Order: 1
→ Status: Enabled
Country flags for switcher — PNG files in catalog/view/image/flags/. Size: 16×11 px or 24×16 px.
4. Set default:
System → Settings → Local
→ Language: Russian
→ Administration Language: Russian (for admin panel)
Language Files and Translations
Custom strings in modules or themes added via language files:
// catalog/language/ru-ru/module/mymodule.php
$_['heading_title'] = 'My extension';
$_['text_add_cart'] = 'Add to Cart';
$_['text_in_stock'] = 'In Stock';
$_['text_out_stock'] = 'Out of Stock';
$_['text_preorder'] = 'Pre-order';
$_['error_required'] = 'This field is required!';
Usage in controller:
$this->load->language('module/mymodule');
$data['text_add_cart'] = $this->language->get('text_add_cart');
In Twig template:
<button>{{ text_add_cart }}</button>
Multilingual Product Content
Each product has description in each language. In database stored in oc_product_description table with language_id field:
SELECT pd.name, pd.description, pd.meta_title, pd.meta_description, l.name as language
FROM oc_product_description pd
JOIN oc_language l ON pd.language_id = l.language_id
WHERE pd.product_id = 123;
In product edit form — language tabs:
Product Edit → "Data" Tab:
[Russian] → name, description, meta tags in Russian
[English] → name, description, meta tags in English
[Belarusian] → name, description, meta tags in Belarusian
If translation missing for language — OpenCart displays default language description.
SEO-URLs for Multilingual
Each language has separate SEO-slug:
-- Russian URL
INSERT INTO oc_seo_url (store_id, language_id, key, value, keyword)
VALUES (0, 1, 'product_id', '123', 'black-office-chair');
-- English URL
INSERT INTO oc_seo_url (store_id, language_id, key, value, keyword)
VALUES (0, 2, 'product_id', '123', 'chernoe-ofisnoye-kreslo');
Or via UI: Product Edit → SEO Tab → Per-language URL keywords.
For correct multilingual SEO structure — set hreflang tags. OpenCart doesn't do this automatically, need custom code or extension:
{# In header.twig add hreflang #}
{% for lang in languages %}
<link rel="alternate"
hreflang="{{ lang.code }}"
href="{{ lang.href }}">
{% endfor %}
<link rel="alternate" hreflang="x-default" href="{{ canonical }}">
Data for languages passed from controller via header controller modification.
Language Switcher
In template — via common/language snippet:
{# In header.twig #}
<div class="language-switcher">
{% for language in languages %}
<a href="{{ language.href }}" class="{% if language.code == language_code %}active{% endif %}">
<img src="{{ language.image }}" alt="{{ language.name }}">
{{ language.name }}
</a>
{% endfor %}
</div>
languages — variable passed from common/header controller. Already contains list of available languages with href for current page.
Currency Configuration
Add currency:
System → Localization → Currencies → Add Currency
→ Currency Title: Belarusian Ruble
→ Code: BYN
→ Symbol Left: (empty)
→ Symbol Right: Br
→ Decimal Places: 2
→ Decimal Point: ,
→ Thousand Point: (space)
→ Value: 0.315 (rate to base currency)
→ Status: Enabled
→ Default: (if BYN — main store currency)
Example for three currencies store oriented to CIS:
| Currency | Code | Symbol | Decimals | Default |
|---|---|---|---|---|
| Belarusian Ruble | BYN | Br | 2 | Yes |
| Russian Ruble | RUB | ₽ | 2 | No |
| US Dollar | USD | $ | 2 | No |
Auto-update rates:
OpenCart supports auto-update via several sources. Default uses ECB (European Central Bank), inconvenient for BYN.
Configuration:
System → Settings → Local
→ Currency Auto Update: ECB (select source)
Manual update:
System → Localization → Currencies → Update Currency Rates
For automatic scheduled update — cron:
# Every day at 9:00
0 9 * * * /usr/bin/php /var/www/myshop/index.php \
route=cron/currency \
cron_token=YOUR_CRON_TOKEN
Or custom script parsing NBRB API:
// shell/update_currencies.php
$nbrb = json_decode(file_get_contents('https://api.nbrb.by/exrates/rates?periodicity=0'), true);
$rates = array_column($nbrb, 'Cur_OfficialRate', 'Cur_Abbreviation');
$pdo = new PDO("mysql:host=localhost;dbname=opencart", 'user', 'pass');
foreach (['USD', 'EUR', 'RUB'] as $currency) {
if (isset($rates[$currency])) {
$stmt = $pdo->prepare(
"UPDATE oc_currency SET value = ?, date_modified = NOW() WHERE code = ?"
);
$stmt->execute([1 / $rates[$currency], $currency]); // BYN as base
}
}
Run via cron, result — current rates in database.
Currency Switcher
Switcher works via POST form to common/currency:
{# In header.twig #}
<div class="currency-switcher">
{% for currency in currencies %}
<form action="{{ action_currency }}" method="post">
<input type="hidden" name="code" value="{{ currency.code }}">
<input type="hidden" name="redirect" value="{{ redirect }}">
<button type="submit" class="{% if currency.code == currency_code %}active{% endif %}">
{{ currency.symbol_left }}{{ currency.symbol_right }} {{ currency.code }}
</button>
</form>
{% endfor %}
</div>
Or via AJAX for change without page reload:
document.querySelectorAll('[data-currency]').forEach(btn => {
btn.addEventListener('click', async function() {
const code = this.dataset.currency
await fetch(actionCurrencyUrl, {
method: 'POST',
body: new URLSearchParams({ code, redirect: window.location.href })
})
window.location.reload()
})
})
Multicurrency Pricing in Catalog
OpenCart stores all prices in base currency and recalculates on-the-fly via $this->currency->format(). This means when exchange rate changes, all displayed prices update automatically.
If you need fixed "nice" prices in each currency (e.g., 999 BYN instead of 998.73) — implement via custom total module or product-level fields with price calculation method override.
Multistore: Different Settings on Different Domains
OpenCart supports multiple stores with shared database:
System → Settings → Add Store
→ Store Name: Russia Store
→ URL: https://myshop.ru/
→ Default Language: Russian (ru-ru)
→ Default Currency: RUB
Each store (store_id) has own language, currency, theme, and product assortment settings. Products linked to specific store via "Links" tab in product editor.
In nginx — one server config, one PHP file, content determined by HTTP_HOST:
server {
server_name myshop.by myshop.ru;
root /var/www/myshop/public_html;
# OpenCart itself determines store_id by domain
}
Common Issues
Catalog prices not updated after rate change. Cause — page caching. Solution: clear cache:
System → Tools → Cache → Clear
Or programmatically:
php index.php route=cron/cache cron_token=TOKEN
SEO-URLs duplicated for different languages. Each oc_seo_url record should have unique combination (store_id, language_id, keyword). Check:
SELECT keyword, COUNT(*) FROM oc_seo_url
GROUP BY keyword HAVING COUNT(*) > 1;
Language switching goes to homepage. Need to pass redirect with current URL when switching. Standard language controller implements this, but some themes remove hidden field from form.
Setup Timeline
- Install languages (2–3) + translate products: 1–2 days
- SEO-URLs for each language + hreflang: 1 day
- Currency setup + auto-update rates (NBRB/CB RF): 0.5–1 day
- Language and currency switchers in theme: 0.5 day
- Multistore (different domains): +1–2 days
Total with ready theme: 2–4 days.







