jjanvier/crowdin-bundle
Symfony bundle that integrates Crowdin translation management into PHP projects. Provides console commands and services to upload source files and download translated content, keeping your application's translation resources in sync with Crowdin throughout development.
The mismatch stands, with one important amendment. jjanvier/crowdin-bundle is still a Symfony2 bundle — no service provider, no config/crowdin.php, no auto-discovery — and still targets the PHP 5.4–5.6 / Symfony 2.x era. But release 0.2.0 changes the shape of the package in a way that matters to a Laravel developer: it now requires php-crowdin-lib as a dependency. The HTTP/Crowdin logic has been extracted into a standalone library, and the bundle is now a thin Symfony wrapper around it.
That split is the headline. You no longer have to choose between "install a 2014 Symfony bundle" and "hand-roll HTTP calls." The realistic path is now: composer require the underlying php-crowdin-lib on its own, skip the bundle entirely, and wrap the library in two Artisan commands.
If you actually have a Symfony 2.x host for the bundle:
composer require jjanvier/crowdin-bundle:^0.2 — note this now pulls php-crowdin-lib transitively, so dependency resolution has one more pin to satisfy (see Gotchas).app/AppKernel.php under registerBundles().app/config/parameters.yml and wire the bundle's config key — check DependencyInjection/Configuration.php for the exact root key and node names, don't guess.bin/console list and look for the command namespace. That output tells you more than the README does.Where to look first, in order (revised): composer.json of the bundle and of php-crowdin-lib (the real capability lives there now), then Command/ (the product surface), then DependencyInjection/Configuration.php, then the README.
First use case: a "download translations" command run against a staging build, populating Resources/translations/ from Crowdin instead of hand-editing.
Laravel-first use case: composer require php-crowdin-lib, expose it as a singleton in a small service provider, and write crowdin:pull / crowdin:push Artisan commands on top. That is the correct call in 2024+ and now involves less code than it did before 0.2.0.
The canonical loop (unchanged — the bundle's whole reason for existing):
messages.en.yml, XLIFF, whatever your format is) and merges to main.Keep upload and download as separate, independently-runnable steps. Never couple them into one "sync" command — you want to pull without ever risking a push.
Ordering matters in CI. Put the download after your build step, or exclude translation directories from cache keys, otherwise you get stale-language builds that are painful to diagnose. If you commit translations, run the download in a dedicated job that opens a PR — translator output stays reviewable and your deploy pipeline stays read-only.
Credentials via environment, always. Any bundle from this era expects parameters in YAML; pass them from env vars so the same config works across staging/production Crowdin projects.
Laravel parity implementation — now library-backed, not hand-rolled. Before 0.2.0 you had to write the HTTP layer yourself. Now you bind the library and delegate:
// app/Providers/CrowdinServiceProvider.php
public function register(): void
{
$this->app->singleton(CrowdinClient::class, fn () => new CrowdinClient(
config('services.crowdin.token'),
config('services.crowdin.project'),
));
}
// app/Console/Commands/CrowdinPull.php
public function handle(CrowdinClient $crowdin): int
{
$buildId = $crowdin->buildTranslations(); // exact method names: read php-crowdin-lib
$zip = $crowdin->downloadBuild($buildId);
// unzip into lang/, one directory per locale, replacing not merging
cache()->forever('crowdin:last-pulled-build', $buildId);
return self::SUCCESS;
}
Two things to keep from the pre-0.2.0 advice: record the build ID you pulled (it's the only proof of which translations shipped in a release), and wrap slow downloads in a queued job.
Treat the library, not the bundle, as your integration surface. Your service provider + commands are ~100 lines and give you the same ergonomics with PHP 8 support and a maintained dependency chain — provided php-crowdin-lib itself resolves on your PHP version. Verify that before committing to the approach (see Gotchas).
Integration tip: make the commands idempotent and safe to re-run. Both CI retries and manual operator reruns will happen, and a download that appends instead of replacing will silently corrupt locale files.
php-crowdin-lib has one more way to fail composer update on PHP 8. Don't expect the release to have modernized the Symfony side.php-crowdin-lib is now the thing to vet. Check its composer.json (PHP constraint, Guzzle major version), its last release date, and — critically — which Crowdin API version it targets. If it's still pre-v2, you inherit the deprecated-endpoint problem without the bundle's code to blame. The release notes here say nothing about API modernization; assume nothing.%name%, :attribute, {count}, and ICU plural blocks before rolling out. Still the #1 source of production sprintf explosions.php artisan tinker and inspect responses — use that before reaching for -v/-vv. Note the commands may still swallow API errors and return a bare non-zero exit; capture the response body on failure. If a command "succeeds" but nothing changes, suspect file-mapping config (source filename, translation file pattern) before auth.execute, or (better) write your own command and use the library directly. Read the service definitions before planning to decorate anything.composer require php-crowdin-lib, bind it in a service provider, write two Artisan commands. If the library's PHP/API constraints are acceptable you get a maintained, PHP 8-ready integration with far less code than before 0.2.0. If they aren't, the fallback is unchanged: fifteen minutes in Command/, then two commands against Crowdin API v2 directly. Either way, the Symfony bundle remains a design reference, not a dependency.How can I help you explore Laravel packages today?