Magento 2 Translations: i18n CSV, Language Packs and JS Strings
Magento 2 has a complete translation system, but strings come from several sources with an order of priority, and JavaScript strings follow a different path from PHP ones. When a phrase stays in English on a translated store, the reason is almost always one of a handful of known causes.
This article explains where translations live in Magento 2.4.8, how they override each other, how to translate strings in PHP, templates, JavaScript and Knockout templates, and how to debug the ones that refuse to change.
How a locale is chosen
Each store view has a locale, set under Stores, Configuration, General, General, Locale Options. The config path is general/locale/code, with values such as en_US, pt_BR or de_DE. The admin panel uses the locale of the admin user, set in their account settings.
Magento then builds a dictionary for that locale by merging translation sources. Multi language stores usually run one store view per language. The hreflang and multilanguage SEO article covers the SEO side of that setup.
Where translations live
There are four sources, listed from lowest to highest priority:
- Module dictionaries in
app/code/Vendor/Module/i18n/pt_BR.csvor the same folder of a Composer module. - Language packages, such as
app/i18n/Vendor/pt_br/or a Composer package of typemagento2-language. - Theme dictionaries in
app/design/frontend/Vendor/theme/i18n/pt_BR.csv, including those inherited from parent themes. - Database translations in the
translationtable, written by inline translation.
A phrase defined in more than one place takes the value from the highest priority source. That makes the theme the right place for wording that is specific to one storefront, and a language package the right place for a full translation shared across projects.
The CSV format
Dictionaries are CSV files with the original phrase in the first column and the translation in the second:
"Add to Cart","Adicionar ao carrinho"
"%1 item(s) in your cart","%1 item(s) no seu carrinho"
"Shipping & Handling","Frete e manuseio"
The match is exact and case sensitive, including punctuation and spaces. Placeholders like %1 must stay in the translation. Quote every field, and escape a double quote inside a field by doubling it. Save the file as UTF-8.
Language packages
A language package is a small component with a language.xml and a registration.php:
<?xml version="1.0"?>
<language xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/Language/package.xsd">
<code>pt_BR</code>
<vendor>vendor</vendor>
<package>pt_br</package>
</language>
<?php
declare(strict_types=1);
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::LANGUAGE,
'vendor_pt_br',
__DIR__
);
The package then holds a pt_BR.csv with all phrases. Community translation packs for many languages are distributed this way, and you can install one with Composer and override only what you need in your theme.
Translating strings in code
PHP and templates
Wrap every user facing string in __(), which returns a Magento\Framework\Phrase that is translated when cast to string:
$message = __('You added %1 to your shopping cart.', $productName);
In templates, translate and escape together:
<h2><?= $escaper->escapeHtml(__('Delivery information')) ?></h2>
Pass dynamic values as arguments, never by concatenating them into the phrase. __('Hello ' . $name) produces a different phrase for every name, so no dictionary entry can match it.
Layout XML
String arguments in layout XML can be translated with translate="true", and many label attributes in system.xml, widget.xml and UI component XML accept the same attribute.
JavaScript
In RequireJS modules, use mage/translate:
define([
'jquery',
'mage/translate'
], function ($, $t) {
'use strict';
return function () {
alert($t('Please select a delivery date.'));
};
});
$.mage.__() does the same thing and is common in older code.
Knockout templates
In .html templates for UI components, use the i18n binding or the translate attribute:
<span data-bind="i18n: 'Order Summary'"></span>
<span translate="'Order Summary'"></span>
Both need the phrase as a literal string inside the binding.
How JavaScript translations reach the browser
PHP strings are translated on the server at render time. JavaScript strings are translated in the browser, using a dictionary file, js-translation.json, generated per theme and locale during static content deploy. Magento builds it by scanning JavaScript and HTML template files for translation calls with literal strings, and including only the phrases it finds that also have a translation in the dictionaries.
This explains most "my JavaScript string is not translated" problems:
- The phrase is built from variables, so the scanner cannot find it.
- Static content was not deployed again for that locale after adding the translation.
- The browser still has an old
js-translation.jsoncached in local storage. Clear local storage for the site and reload.
The strategy is set by dev/js/translate_strategy, with dictionary as the default and embedded as the alternative, which writes translations directly into the deployed JavaScript files. The dictionary strategy is the usual choice.
Collecting phrases
To produce a CSV with every phrase in your code that needs a translation:
bin/magento i18n:collect-phrases -o app/code/Vendor/Module/i18n/en_US.csv app/code/Vendor/Module
This scans PHP, templates, layout, JavaScript and HTML templates in that folder. Give the file to your translators, then save their version as pt_BR.csv or the matching locale. To collect phrases from the whole installation for a full language package, use the --magento flag with the root path. Generating a full pack with i18n:pack is mostly useful for maintainers of language packages.
Inline translation
Inline translation lets an admin user edit phrases directly on the storefront. Enable it under Stores, Configuration, Advanced, Developer, Translate Inline, which is only visible in developer mode. Changes are saved to the translation table and win over every CSV.
It is handy for a quick fix on a staging site, but it scatters translations in the database where code review and version control cannot see them. Do not enable it in production. If someone did, export the translation table, move the entries into the theme CSV, and clear the table.
Applying changes
bin/magento cache:clean translate full_page block_html
bin/magento setup:static-content:deploy pt_BR en_US -f
PHP side translations only need the translation cache cleaned. JavaScript translations also need static content deployed for the locale. In production mode, run static content deploy for every locale in use as part of the normal build. The static content deploy strategies article covers how to keep that fast with several locales.
Debugging a phrase that will not translate
- Confirm the store view locale is the one you expect.
- Find the exact source phrase in code, with case, spacing and punctuation, and compare it with your CSV.
- Check that the code actually calls
__()or$t(). Some third party templates output hardcoded English. - Look for a higher priority source with a different value, especially the
translationtable. - For JavaScript, search the deployed
js-translation.jsonfor the phrase. - Clean the translation cache, and for JavaScript, deploy static content and clear local storage.
For theme level dictionaries and how themes inherit them, see the child theme guide.
FAQ
Should translations go in the module or the theme?
Ship default translations with the module that owns the strings. Put store specific wording in the theme, where it overrides module and language package translations.
Why is a phrase translated in PHP but not in JavaScript?
JavaScript uses js-translation.json, which is generated during static content deploy. Deploy static content for the locale again and clear the browser local storage.
Can the same phrase have different translations in different modules?
In practice, no. The dictionary for a locale is shared, so one source phrase ends up with one translation on the storefront. Use a different source phrase if you need different wording.
Is inline translation safe for production?
No. It writes translations to the database outside version control and adds editing markup to pages. Use it only on development or staging environments.
I am available for freelance Magento and Adobe Commerce work, including multi language store setups. If you need help with this, get in touch.
Related articles
Writing Alpine.js Components in Hyvä: Patterns That Scale
How to write Alpine.js components on Hyvä: passing PHP data, view models, customer data events, form keys, component events, CSP and keeping templates clean.
Hyvä Compatibility Modules: Make Luma Extensions Work
How Hyvä compatibility modules work: the CompatModuleRegistry, rewriting extension templates with Alpine.js, hyva_ layout handles and the Luma fallback.
Hyvä Tailwind Customization: Colors, Fonts and Safelists
Customize Tailwind CSS in a Hyvä theme: content paths, brand colors and fonts in v3 and v4, @apply components, CMS classes, safelists and keeping CSS small.
Hyvä Theme Setup Guide for Magento 2.4.8: Install to Deploy
Step by step Hyvä setup on Magento 2.4.8: Composer install, child theme, Tailwind build, hyva:config:generate, deploy order and the usual first project issues.