it-blaster/ru-strings-bundle
it-blaster/ru-strings-bundle is a Laravel/PHP package that bundles Russian string helpers and localization utilities, making it easier to handle Russian text, pluralization, and language-specific formatting in your applications.
Do not composer require this package. It's a Symfony 2.x bundle: it ships a Bundle class registered in AppKernel, DI YAML/XML service definitions resolved by Symfony\Component\DependencyInjection, and a Twig extension. Laravel's ServiceProvider, Illuminate\Container, and Blade have no overlap with any of that, and pulling Twig + Symfony DI into a Laravel app just to get Russian declension is not a trade a working dev should make. Treat the repository as MIT-licensed reference source you extract from.
Where to look first. Clone the repo and open three files before writing any code:
Twig/ — the Twig extension's getFilters() is the real public API. Filter names, argument order, and the three-form array convention live here. The README is typically out of date.Service/, Util/, or Helper/. This is the whole reason you're here: it holds the plural-concord rules (1 день / 2 дня / 5 дней) and the case-declension tables (nominative → genitive → dative …).composer.json — check the PHP constraint and autoload style. Expect PHP 5.x-era constructs and possibly PSR-0; you will need to adapt before it runs on PHP 8.Your first use case is almost certainly plural agreement, not declension. The overwhelming need in day-to-day Laravel work is a cart counter, a comment count, a "N days remaining" badge. Before you port anything, check whether Laravel's trans_choice already covers it:
// resources/lang/ru/cart.php
return ['items' => '{0} нет товаров|{1} :count товар|[2,4] :count товара|[5,*] :count товаров'];
{{ trans_choice('cart.items', $count, ['count' => $count]) }}
That covers the "easy half" of Russian pluralization natively, is maintained, and is already in your stack. Stop here if plural concord is all you need. Only continue to porting if you require case declension — declining a user's name or a city name into genitive/dative ("письмо Ивану", "в Москве") — which trans_choice cannot do.
Minimal extraction, if you truly need declension:
app/Support/RussianStrings/Decliner.php (or a private Composer package acme/ru-strings).use statement and __construct signature. Keep only the pure string logic.ru.decliner and exposes a Blade directive @ruDecline('Иван', 'dative') and a helper ru_plural($n, $forms).Pattern 1 — Plural agreement via trans_choice (preferred). Put the Russian strings in resources/lang/ru/ and use Laravel's native range selectors. This is the pattern you should default to; it needs no extracted package at all.
// resources/lang/ru/notifications.php
return [
'days_left' => '{0} срок истёк|{1} остался :count день|[2,4] осталось :count дня|[5,*] осталось :count дней',
];
<span class="badge">{{ trans_choice('notifications.days_left', $days, ['count' => $days]) }}</span>
Pattern 2 — Wrap the extracted decliner behind an interface. Do not leak the copied class into controllers and views. Bind it, inject it, and let the interface be the seam if you later swap the implementation for petrgrishin/name-case or similar.
namespace App\Support\RussianStrings;
interface Decliner
{
/** @param string $case one of: nom, gen, dat, acc, ins, pre */
public function decline(string $word, string $case): string;
/** @param string[] $forms exactly three: [one, few, many] */
public function plural(int $n, array $forms): string;
}
// app/Providers/RussianStringsServiceProvider.php
public function register(): void
{
$this->app->singleton(Decliner::class, DeclinerImplementation::class);
}
Pattern 3 — Expose it to Blade as a directive and a helper. Views shouldn't app() into the container.
// in the same ServiceProvider's boot()
Blade::directive('ru', function ($expression) {
return "<?php echo e(app(App\Support\RussianStrings\Decliner::class)->decline{$expression}); ?>";
});
<p>Здравствуйте, @ru('Иван', 'nom')!</p>
<p>Написать @ru('Иван', 'dat')</p>
Pattern 4 — Compose with translation for personalized copy. Declension is applied to the subject of a fixed string; keep the sentence template in resources/lang/ru/ so translators (or a Russian PM) can edit wording without touching code.
// resources/lang/ru/email.php
return ['greeting_to' => 'Письмо для :name'];
$addressee = app(Decliner::class)->decline($user->first_name, 'dat');
echo __('email.greeting_to', ['name' => $addressee]); // "Письмо для Ивану"? — no: "Письмо для Ивана" (genitive)
Note the case you need depends on the sentence's grammatical role, not the sentence's topic — check with a native speaker which case each slot takes.
Pattern 5 — Cache list rows if you render many per request. Not because string manipulation is slow, but because you'll end up declining the same product names in a loop. Cache::rememberForever("ru.dcl.{$case}.{$word}", fn () => ...) is a cheap insurance and makes the pattern obviously safe in code review.
Integration tips.
Request, no app(), no Config. All framework glue lives in the service provider and the Blade directive.The 11–14 trap. Russian plural rules have a hard exception: 11–14 take the "many" form, and numbers ending in 11–14 take "many" regardless of the last digit's normal rule. "11 товаров", not "11 товара". "21 товар", "22 товара", "25 товаров". Verify your extracted plural() handles n % 100 in [11,14] before falling through to the n % 10 rules. This is the single most common source of visibly wrong Russian on counters.
Zero is not one. {0} нет товаров |{1} … — treat 0 as a distinct form (many), not as singular. Laravel's trans_choice range selectors let you express this cleanly; a naive $n == 1 ? one : (($n >= 2 && $n <= 4) ? few : many) gets 0 wrong.
Decimals. 1.5 товара — you need the genitive singular, and some libraries silently fall through to "many". If your UI can show fractional counts (money, weights, ratings), write a test.
Silent failure on proper nouns. The declension class almost certainly uses suffix rules, not a dictionary. It will produce something for Ковальски, Хемингуэй, Биг-Бэн, or a Latin-script name, and it will be wrong without raising an error. This is the most dangerous failure mode: bad grammar copied across every rendered page with no exception to alert you. Either (a) accept the copywork and avoid declining user-supplied names where phrasing allows — "Здравствуйте, Иван" instead of "Письмо Ивану" — or (b) gate declension of user input behind a whitelist of known-safe noun types.
PHP 8 won't run it. Symfony 2.x-era code frequently uses each(), create_function(), untyped signatures, and PSR-0 autoloading — all removed or broken in PHP 7/8 and Composer 2. Your extraction must strip these, and composer.json constraints must be rewritten to "php": "^8.1". Expect fatal errors if you vendor the package verbatim.
No ServiceProvider, no Blade filters. The Twig extension's getFilters() will not magically exist in Blade. Do not try to register the Twig extension; write a thin Blade directive that calls your own service. Filter names to check in the source: typically ru_plural, ru_case, plural_ru — the actual name must come from getFilters(), not the README.
The bundle will not register in a modern Laravel app. There is no auto-discovery (no extra.laravel.providers in composer.json), and AppKernel::registerBundles() is not a thing in Laravel. Any tutorial referencing it is describing a Symfony app.
Escaping. Twig autoescapes filter output; Blade uses {{ }} for escaped and {!! !!} for raw. If your directive emits HTML-unsafe text (declined Cyrillic is fine, but names with quotes are not), route it through e() inside the directive to match expectations.
The MIT license is your friend. Fork, vendor, modify — no obligation to upstream. But own it completely: 0 stars and 0 dependents means no issue triage, no CVE feed, no maintainer. Every future PHP/Laravel bump is your ticket to file. Budget it as a small internal utility, not a dependency.
Extension points worth keeping. Preserve the original's dictionary/override mechanism if it has one — the ability to add custom indeclinables ("кофе", "метро", "пальто" don't decline) or domain-specific nouns. That dictionary is exactly where your app's domain vocabulary lives and where a future translator will want to edit without touching algorithm code.
Linguistic sign-off is the real gate. A mid-level Laravel dev can extract and wire the logic in a day or two. The underestimated cost is validation: someone with native Russian fluency needs to review your test corpus — nouns across all three genders, animacy, indeclinables, and the 0 / 1 / 2–4 / 5–20 / 21 / 1.5 boundaries — before any of it ships in user-facing copy. Build that review into the plan, not the post-ship bug list.
How can I help you explore Laravel packages today?