ruwork/template-i18n-bundle
Symfony bundle that brings internationalization to templates, letting you translate template strings and manage locale-aware content. Integrates with Symfony's translator and Twig to keep multi-language templates clean and organized.
First, a framework reality check: ruwork/template-i18n-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/template_i18n.php. If you are working in a Laravel codebase you cannot composer require this and expect it to boot — you would be porting concepts, not code. The rest of this assessment assumes a Symfony application (or a deliberate evaluation of whether to port).
Minimal steps in a Symfony app:
composer require ruwork/template-i18n-bundle
Then register the bundle — config/bundles.php on Symfony 4+, AppKernel::registerBundles() on Symfony 3:
return [
// ...
Ruwork\TemplateI18nBundle\RuworkTemplateI18nBundle::class => ['all' => true],
];
Where to look first: the bundle is small, so read the DependencyInjection/Configuration.php and the bundle's README in that order. The config root key and the service IDs it wires up are the real documentation — the README on a 2018 bundle with 0 stars is typically a stub. Identify (a) the locale/fallback options, (b) which service is the template resolver/loader, and (c) whether it decorates Twig's loader or exposes a standalone resolver.
First use case: localized transactional email templates. You have templates/email/welcome.html.twig and want templates/email/welcome.ru.html.twig (or a locale subdirectory) selected automatically from the request locale, with a fallback to the default. That is the canonical reason to reach for a template-i18n bundle, and it is the fastest way to confirm the package does what you think it does.
// quick smoke test in a controller
$html = $this->renderView('email/welcome.html.twig', ['user' => $user]);
// with the request locale set to 'ru', you expect the .ru variant to win
If that doesn't behave as expected on the first try, stop and read the loader before building anything on top of it.
Locale-aware template resolution. The bundle's core value is picking a template variant from the active locale. Wire the locale from the request ($request->getLocale()) or from the security/user context for async work (queued mailers, console commands) where there is no request. Always set it explicitly before rendering:
$renderer->render($template, $parameters, $locale); // or via RequestContext / LocaleAwareInterface
Fallback chain over "one file per locale." Do not duplicate the entire template tree per locale. Structure it so the base template holds layout and the variant only overrides blocks or strings:
templates/email/welcome.html.twig # base, default locale
templates/email/welcome.ru.html.twig # overrides only what changes
This keeps drift manageable and makes diff review sane when marketing changes copy in one locale.
Separate strings from structure. Use this bundle for template selection, and Symfony's Translator ({{ 'email.welcome.subject'|trans }}) for string translation. Mixed responsibilities are the number-one source of "why is my Russian email half-English" bugs. In Laravel terms: this bundle is a view-resolver concern; __() is a translation concern — same split.
Service injection over Twig trickery. Inject the resolver/rendering service into mailers, notification services, and event listeners rather than calling Twig globals from templates. Template selection is application logic, and it is far easier to test when it lives in a service:
final class WelcomeMailer
{
public function __construct(private TemplateI18nRenderer $renderer) {}
public function send(User $user): void
{
$body = $this->renderer->render('email/welcome.html.twig', ['user' => $user], $user->getLocale());
// ...
}
}
Test the locale matrix explicitly. A small data-provider test per critical template (default, ru, unknown locale → fallback) catches most regressions. Assert on the rendered output, not on which file was loaded — the latter couples you to internals.
Laravel port pattern (if you go that way). The equivalent is a custom view resolver or a helper that checks view()->exists() against a locale-suffixed name before falling back:
function localized_view(string $name, array $data = [], ?string $locale = null): View
{
$locale = $locale ?? app()->getLocale();
foreach (["{$name}.{$locale}", $name] as $candidate) {
if (view()->exists($candidate)) {
return view($candidate, $data);
}
}
throw new InvalidArgumentException("No view for [{$name}]");
}
That is roughly 10 lines and covers 90% of what this bundle offers, which is worth weighing against adopting an unmaintained dependency.
composer resolution or fail at runtime on removed APIs. Pin-and-patch only if you accept owning the code.bundles.php, then bin/console cache:clear. Symfony caches bundle metadata aggressively.cache:clear / cache:pool:clear) — otherwise the fallback template keeps winning and you'll chase a ghost.Translator. If you find yourself putting whole sentences into template files per locale, you are fighting the tool. Structural variants belong here; strings belong in translations/.How can I help you explore Laravel packages today?