ibexa/i18n
The official translation meta-repository for Ibexa, centralizing locale files synced from Crowdin so translations ship with the product. Enable them via the ibexa.ui.translations.enabled configuration, and contribute new translations through the Ibexa project on Crowdin.
ibexa/i18n is not a library you call — it is a meta repository of translation resources that is machine-synchronised from Crowdin into the product. Your job as a developer is almost always consumption and overriding, not integration.
Install / confirm it is present
It normally arrives transitively with ibexa/oss, ibexa/experience, or ibexa/content:
composer show ibexa/i18n # check version tag
ls vendor/ibexa/i18n
Avoid requiring it explicitly in composer.json unless you need to pin a translation snapshot; let the metapackage constraint drive it.
Enable the translations
The README gives the only configuration knob:
# config/packages/ibexa.yaml
ibexa:
ui:
translations:
enabled: true
Then clear the cache so the translator warms its catalogue:
bin/console cache:clear
Where to look first
find vendor/ibexa/i18n -path '*translations*' -name '*.y*ml' (or *.xlf) to see the actual resource files and locale codes.grep -rn "addResource\|translations" vendor/ibexa/i18n/src to find the DI extension that registers the path — that tells you the translation domain and load order.bin/console debug:translation <locale> --domain=<domain> to confirm a key resolves and which file wins.First use case
Enable a second language on a siteaccess and verify a back-office string is translated:
ibexa:
system:
my_site:
languages: [eng-GB, ger-DE]
Then check in Twig, using the domain the extension registers (commonly ibexa, but confirm rather than assume):
{{ 'ibexa.some.key'|trans({}, 'ibexa') }}
If you see the raw key, the catalogue was not loaded — see Gotchas.
Consume keys, never copy files. Importing vendor translation files into your project forks them; the next composer update ibexa/i18n resurrects duplicates and your debug:translation output becomes ambiguous.
Override strings in your app, not in vendor. Put translations/<domain>.<locale>.yaml (e.g. translations/ibexa.en.yaml) in the project root. Symfony's app catalogue is loaded last, so it wins on key collisions. Always verify empirically:
bin/console debug:translation en --domain=ibexa | grep -A3 "my.key"
Namespace your own keys. Use a project prefix (app.*, acme.*) in a project-specific domain and leave the vendor domain untouched. Mixing your keys into the ibexa domain makes future upstream key renames silently shadow your text.
Custom code pattern
// anywhere the TranslatorInterface is injected
echo $translator->trans('app.invoice.title', ['%id%' => $id], 'app');
{# in the admin UI / front end #}
{{ 'app.invoice.title'|trans({'%id%': id}, 'app') }}
In Ibexa templates you may also see the legacy extractor filter used for key collection — keep the key text identical to the label so extraction stays stable:
{{ 'app.invoice.title'|trans|desc('Invoice title') }}
Locale strategy for unsupported languages. If your locale (say nl-BE) has no catalogue, map it to the closest supported one rather than leaving keys raw:
framework:
translator:
fallbacks: [en]
default_locale: en
You can register a fallback chain per locale (nl_BE → nl → en) so a partially translated UI degrades instead of exploding into keys.
Workflow for new/updated translations
composer update ibexa/i18n && bin/console cache:clear in CI and locally.CI check that costs one line
Fail the build if a required key is untranslated in a shipping locale:
bin/console debug:translation de --domain=ibexa --only-missing
This repo is generated — direct PRs get clobbered. The README describes an automated pipeline (Crowdin → l10n_main → PR → squash-merge to main). Any edit you push to a translation file is either rejected or lost on the next sync. Fix translations on Crowdin, or override them in your project.
The license is NOASSERTION, and it is not a simple permissive license. The README explicitly splits licensing: Ibexa BUL v2.3+ (with product edition determining terms). Do not extract, rehost, or redistribute the translation catalogue as a standalone dataset — treat it as product content. Get legal sign-off before shipping a derived translation package.
enabled: false (or missing) is the #1 "why are my strings raw" cause. The config is required for the resources to be registered; the package alone does nothing. It also interacts with your siteaccess languages list — a locale not in the siteaccess languages will not be negotiated.
Cache is the #2 cause. Symfony caches catalogues per environment under var/cache/<env>/translations. After a composer update or an override file change, run bin/console cache:clear. If you use a cache pool (Redis/APCu, e.g. in Ibexa Cloud), clear the pool too — otherwise warm-up writes stale entries back.
Locale code mismatch is silent. Symfony uses pt_BR, de_DE, zh_CN style codes and matches filenames exactly. pt-BR in framework.default_locale will not find ibexa.pt_BR.yaml; you get the fallback, not an error. Same trap for the siteaccess languages codes Ibexa uses (eng-GB, ger-DE, por-PT style legacy codes) — check what the siteaccess normalises to before filing a bug.
Load-order ambiguity when overriding. Bundle translations and app translations can be registered in an order that varies by Symfony version and by how the bundle extension adds its path. Never assume your translations/ibexa.en.yaml overrode the vendor key — confirm with debug:translation, and if it loses, register your catalogue explicitly at higher priority in a custom extension rather than fighting the default.
JS strings in the back office travel a different pipeline. Admin UI JavaScript labels are usually extracted and served from a generated catalogue (JSON/translations.js), not the PHP translator. Correcting a YAML entry may not change a label rendered entirely client-side; look for the extraction/warm-up step in the Ibexa build before you spend an afternoon on it.
Pluralization needs intl. ICU plural/select syntax and the | message-choosing format will quietly misbehave without the intl PHP extension. Check php -m | grep intl in every environment image, including CI and local Docker.
Missing keys are not errors. You get the key string back. Add debug:translation --only-missing and a "no unresolved keys" assertion on the paths that matter (checkout, login, notifications) — not on everything, or the check becomes noise.
Low adoption signals a support boundary, not a quality problem. 3 stars and 0 dependents means there is no third-party documentation, no Stack Overflow corpus, and no external API stability contract. Expect to read the DI extension and Symfony's translator source when behaviour surprises you, and pin to release tags (do not track main/l10n_main) so a sync commit cannot change your UI text mid-sprint.
Extension points to remember: the config flag ibexa.ui.translations.enabled, app-level translations/ files, a custom TranslatorInterface decorator or a MessageCatalogue manipulator if you need per-siteaccess text variation, and Crowdin itself for anything that should live upstream.
How can I help you explore Laravel packages today?