DEV Community

Magevanta
Magevanta

Posted on Originally published at magevanta.com

Magento 2 Translation & i18n Performance: The Hidden Bottleneck Nobody Tunes

Every Magento 2 performance audit covers Redis, Varnish, PHP-FPM and the database. Almost none cover translation. That's a mistake — especially if your store ships in more than one language. The i18n stack sits in the request path of every single page: before the layout renders, before a block prints a heading, before checkout loads, Magento has to resolve the translated string. For an en_US store that's nearly free. For everything else, it's a silent tax of tens to hundreds of milliseconds per request, and on a cold cache it can be seconds.

This post covers how Magento 2 translations actually work, the four places where i18n hurts performance (inline translation, the translation table, dictionary parsing on cache misses, and JS translation files), how to measure the damage, and a concrete checklist to fix it.

How Magento 2 translation works under the hood

Every UI string goes through Magento\Framework\Translate. On each request in a given area, the translate object collects phrases from several sources and merges them into one in-memory dictionary:

  1. Database translations — the translation table, keyed by identifier (the source string), locale and scope (store view / default).
  2. Module dictionariesapp/code/<Vendor>/<Module>/i18n/<locale>.csv.
  3. Theme dictionaries<theme>/i18n/<locale>.csv.
  4. JS translations — pre-built js-translation.json files, generated during static content deploy.

The merge order defines precedence: DB overrides win, then theme, then module. That merged dictionary is then stored in the translate cache type (Redis by default), so the expensive part only happens on a cache miss.

Two multipliers make this expensive:

  • Locale fallback. If your store uses fr_FR and a string isn't translated, Magento falls back to the default locale (en_US). The fallback chain effectively doubles the number of dictionaries that must be loaded — and on a cold cache, every module and theme dictionary for both locales gets parsed.
  • Theme fallback. A child theme falls back to its parent. Each level adds more CSV files to the parse set.

On a typical install with 300+ modules and a custom child theme, a single cold translate-cache miss means parsing hundreds of CSV files — tens of megabytes of text, multiple seconds of CPU. The translate cache hides this, which is exactly why it hurts most right after a deploy or cache flush.

1. Inline translation: the biggest footgun

Magento's inline translation mode (Store → Configuration → Developer → Translate Inline) is meant for developers working in developer mode. It wraps every translatable string in a &lt;span data-translate&gt; element and lets you click it in the browser to edit — the edit is then written to the translation table.

What most people don't realize:

  • While inline translation is active, translation caching is bypassed. The merged dictionary is rebuilt on every request. You don't just pay for spans in the HTML — you pay for full CSV parsing on every single page load.
  • It only works in developer mode, but the config flag is per store view and can be left on when the store is switched to production. In production mode the spans aren't rendered, but the config toggle itself is a trap for the next person who switches modes to debug something.
  • Every click in the editor creates a row in translation with store scope. A quick "let me fix five labels" session inserts five rows that now live in the database instead of in version-controlled CSV files.

The fix: keep it off, everywhere, permanently.

bin/magento config:set dev/translate/inline/active 0
Enter fullscreen mode Exit fullscreen mode

If you're doing translation work, do it in CSV dictionaries and deploy them — see the checklist at the bottom. Never use the inline editor in production, and never rely on it as your translation workflow at all.

2. The translation table: the database time bomb

The translation table is small on a healthy install — a handful of store-scope overrides. But it's one of the most common tables to find bloated in the wild, for three recurring reasons:

  • Magento 1 migrations. M1 stored translations in the database by default (core_translate), and migration tools happily copy them over. Stores that ran M1 for years arrive with tens of thousands of rows, most of them identical to the file dictionaries.
  • Inline translation experiments (see above).
  • Import scripts that push a full translated catalog through SQL inserts instead of CSV files.

Why does it matter? On every request, the translate object loads the DB translations for the current locale and scope. The translation table isn't queried per phrase — it's fetched as the relevant row set for that locale/scope and merged into the dictionary. A bloated table means:

  • Larger merged dictionary in memory on every request (and in the cached payload).
  • Slower SELECT on the translation table itself, especially if rows were inserted via INSERT ... ON DUPLICATE KEY UPDATE patterns that left the table fragmented or with a suboptimal index.
  • Admin pages get slow too — the admin area loads its own locale dictionaries, and a multi-hundred-thousand-row table makes every admin request drag.

The fix: audit and clean.

-- Size check
SELECT locale, scope, COUNT(*) FROM translation GROUP BY locale, scope ORDER BY COUNT(*) DESC;

-- A healthy install: only real store-scope overrides remain.
-- If you see tens of thousands of rows per locale, that's the M1 migration problem.
Enter fullscreen mode Exit fullscreen mode

Keep only genuine overrides, move the rest into file dictionaries, then:

OPTIMIZE TABLE translation;
Enter fullscreen mode Exit fullscreen mode

In Magento 2.4.6+, the table uses VARCHAR identifiers and has a composite index on (identifier, locale, scope). Verify with SHOW INDEX FROM translation and re-add the composite index if a migration left it without one — a table scan on a 500k-row translation table during a cache rebuild will stall the whole site.

3. Dictionary parsing and the translate cache

Even with a clean database, the file-dictionary layer deserves attention. The translate cache type is Redis-backed by default, with a default lifetime. The failure mode is a cold cache after deploy: cache:flush, setup:upgrade, or a Redis eviction under memory pressure forces a full rebuild of the merged dictionary. On a large module set with locale + theme fallback, this rebuild is one of the slowest cache builds in the entire application — worse than config or layout, because it touches the most files.

Two things make it worse:

  • A Redis maxmemory policy that evicts before expiry (allkeys-lru etc.). Translation dictionaries are large but infrequently re-read, so they're prime eviction candidates. If your Redis is constantly evicting, you're paying the CSV parse bill over and over. Give Redis enough headroom or assign a dedicated DB for cache data.
  • Too many locales. Every enabled locale multiplies the dictionary set. If you have five store views in three locales, you need three locales deployed and parsed — but dozens of locale folders in app/i18n are only parsed when referenced.

The fix: treat the translate cache like any other critical cache.

  • Warm it after every deploy or cache flush (see the cache warming guide for the pattern — a quick crawl of one representative page per store/locale rebuilds all dictionaries up front, before real traffic hits).
  • Monitor Redis evictions (INFO statsevicted_keys). If it's climbing, fix memory sizing before blaming Magento.
  • Remove unused locales from the codebase. Every i18n/<locale>.csv you don't use is just dead parse weight on fallback chains.

There's also an offline shortcut for large dictionaries: bin/magento i18n:pack compiles a master CSV into the packed CSV format, which parses faster and is the format shipped with the official Magento language packs. If you maintain a big custom dictionary, pack it:

bin/magento i18n:collect-phrases -o /tmp/all_phrases.csv
bin/magento i18n:pack /tmp/all_phrases.csv <Vendor>_<Module>/i18n/fr_FR.csv
Enter fullscreen mode Exit fullscreen mode

4. JS translations and static content

Frontend strings ship to the browser via js-translation.json, one per theme/locale, generated as part of setup:static-content:deploy. This is where deployment mistakes show up as visible slowness:

  • If the deploy ran without the locale flag and your store uses a non-default locale, the JSON for that locale doesn't exist.
  • With MAGE_MODE=developer (or a misconfigured mode), Magento falls back to on-the-fly generation: the JSON is rebuilt from parsed dictionaries on the first request, per theme, per locale. That first request can take seconds, and under concurrent traffic the regeneration thrashes CPU.

The fix: deploy explicitly, for exactly the locales you run.

bin/magento setup:static-content:deploy -f -l en_US fr_FR nl_NL
Enter fullscreen mode Exit fullscreen mode

And verify the files exist after deploy:

find pub/static -name 'js-translation.json' | head
Enter fullscreen mode Exit fullscreen mode

Then check that the store's theme/locale combination is actually covered — store views referencing a locale that was never deployed are the classic cause.

Measuring the damage

Don't tune blind. Three quick ways to see what translation is costing you:

  1. Profiler. bin/magento dev:profiler:enable, load a category page in a non-en_US store view, and check the translate section — dictionary load time and DB translation load queries. Compare the same page in en_US.
  2. Redis hit ratio. redis-cli INFO stats → compare keyspace_hits against keyspace_misses. A low overall ratio combined with evicted_keys > 0 means the translate cache (among others) is being rebuild constantly.
  3. Slow query log. Enable MySQL's slow query log at 0.5s and filter for the translation table. If translation shows up at all, the table is too big or missing its index.

A healthy multilingual store should show zero translation-table queries in the slow log, a warm translate cache with minimal rebuilds, and no measurable difference between en_US and translated store views on a warmed cache.

The checklist

  • [ ] dev/translate/inline/active is 0 everywhere; no spans in production HTML.
  • [ ] translation table audited: only real store-scope overrides, OPTIMIZE TABLE run, composite index present.
  • [ ] All translations live in version-controlled CSV dictionaries, packed with i18n:pack; no DB imports.
  • [ ] Static content deployed explicitly per locale; js-translation.json exists for every theme/locale in use.
  • [ ] Redis sized so evicted_keys stays near zero; no premature translate-cache eviction.
  • [ ] Translate cache warmed after every deploy or cache flush.
  • [ ] Unused locales removed from the codebase; theme fallback chain kept shallow.

Translation is the rare performance topic where the fixes are all config, hygiene and process — no custom modules required. An hour of cleanup removes hundreds of milliseconds from every request in every store view that isn't en_US, and it makes the next deploy measurably faster too. That's about as cheap as Magento performance wins get.

Top comments (0)