Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Crowdin Api Laravel Package

akeneo/crowdin-api

A PHP client for the Crowdin API that makes it easy to manage translation projects, files, strings, and localizations from your application. Provides a clean, typed wrapper around Crowdin endpoints for automating localization workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

This is still a plain PHP SDK, not a Laravel package — no service provider, facade, or config file to publish. But v2.0.0 replaces Guzzle with Symfony's HttpClient, which changes your integration surface significantly. Plan to wire it yourself.

  1. Install:
    composer require akeneo/crowdin-api
    
    Version check first: 2.0.0 pulls in symfony/http-client ^6.0. That means PHP 8.1+ and effectively Laravel 9+ (Laravel 8 ships Symfony 5 and will conflict). If you're stuck on Laravel 8, pin to the 1.x line and accept the Guzzle-based assessment from before.
  2. Read src/ before the README. The README is thin; the src/ directory is the real documentation. In 2.0.0 look specifically at how the client constructor now accepts an HttpClientInterface — if it does, you can inject your own (with retries, logging, timeouts baked in) rather than letting the SDK build one.
  3. Generate a Crowdin Personal Access Token (or OAuth token) with the project scopes you need, and note your numeric project ID.
  4. Register it as a singleton so you don't rebuild the client per request. Now you can also bind a pre-configured HttpClientInterface:
    // config/services.php
    'crowdin' => [
        'token'      => env('CROWDIN_TOKEN'),
        'project_id' => env('CROWDIN_PROJECT_ID'),
        'base_uri'   => env('CROWDIN_BASE_URI', 'https://api.crowdin.com'),
    ],
    
    // App\Providers\AppServiceProvider::register()
    $this->app->singleton(\Symfony\Contracts\HttpClient\HttpClientInterface::class, function () {
        return new \Symfony\Component\HttpClient\RetryableHttpClient(
            \Symfony\Component\HttpClient\HttpClient::create([
                'timeout'         => 60,
                'max_duration'    => 300,
            ]),
            new \Symfony\Component\HttpClient\Retry\GenericRetryStrategy(
                maxRetries: 3,
                delayMs: 500,
            ),
        );
    });
    
    $this->app->singleton(\Crowdin\Client::class, fn ($app) => new \Crowdin\Client(
        config('services.crowdin.token'),
        config('services.crowdin.base_uri'),
        // pass $app->make(HttpClientInterface::class) if the constructor accepts it
    ));
    
    Adjust class name / constructor signature to whatever src/ actually exposes — this is the part most likely to have changed in 2.0.0.
  5. First use case: read-only. Before you write anything, pull data — list projects, fetch project details, or download an existing translated file. This validates auth, base URI, and response shape without risking writes.

Implementation Patterns

Wrap the client in your own service. Never inject the raw SDK into controllers. Create an App\Services\CrowdinService that exposes domain methods (syncProducts(), downloadTranslations(string $locale)) and returns your own DTOs/arrays. This isolates the SDK's exception types and lets you swap it later — even more important now that the HTTP layer has changed under you once already.

namespace App\Services;

use Crowdin\Client;

class CrowdinService
{
    public function __construct(private Client $client) {}

    public function translationsFor(string $locale): array
    {
        // translate SDK calls into your own shape here
    }
}

Push/pull as queued jobs. Crowdin operations are network-bound and can be slow (large file uploads, translation builds). Keep controllers fast:

SyncTranslationsJob::dispatch($locale)->onQueue('translations');

Give each job $timeout = 300 and $tries = 3 with backoff() — Symfony HttpClient will still occasionally throw transport timeouts, and you want them retried at the job layer.

Configure retries at the HTTP client, not the job. With v2.0.0 you get Symfony's RetryableHttpClient, which retries idempotent failures (5xx, timeouts) transparently. Wrap the base client once in the container as shown above. Do not also retry 5xx yourself in jobs or you'll multiply the load on Crowdin's side during incidents.

Storage API is a two-step flow. Crowdin doesn't accept raw file bodies on most file endpoints. You upload to the Storage endpoint first, get a storage ID back, then reference that ID when creating/updating a source file. Model this as two methods in your service, not one, so you can retry the second step independently. Note that with RetryableHttpClient the upload itself will be retried on transient failures, but you shouldn't blindly retry the file-create step (it's not idempotent — you can create duplicates).

Scheduled sync via Artisan. A php artisan crowdin:sync command scheduled in Kernel/routes/console.php is the cleanest day-to-day ergonomic for keeping translations fresh. Inside the command, call your service — not the SDK directly.

Webhooks instead of polling. Configure Crowdin project webhooks to hit a route in your app on translation.updated / file.translated. Verify the signature header before trusting the payload, then dispatch a job that pulls the changed file. This replaces expensive polling loops. The HTTP client change doesn't affect this, but the webhook handler should hand off to your service, not reach into the SDK.

Testing — the big change. Guzzle's MockHandler is gone. You now test with Symfony\Component\HttpClient\MockHttpClient and MockResponse:

use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

$mock = new MockHttpClient([
    new MockResponse(json_encode(['data' => ['id' => 1]]), ['response_headers' => ['content-type' => 'application/json']]),
]);

// bind $mock into the container in place of HttpClientInterface, then resolve Crowdin\Client

Simpler still: bind a fake CrowdinService in the container and skip the SDK entirely for unit tests. Keep an integration test suite (separate group, run in CI nightly) against a sandbox Crowdin project — especially important right after a major version, since the response handling is fresh code now.

Gotchas and Tips

  • 2.0.0 is a major release with a different HTTP stack. If you built on 1.x, expect constructor signature changes, exception class changes, and any place you passed a Guzzle client or options to now be dead. Diff src/ and the changelog before upgrading, and budget half a day for the migration.
  • Symfony 6 dependency locks you to Laravel 9+. Verify your Laravel/PHP matrix on the target environment before composer update. Older apps cannot use 2.0.0 without also upgrading Symfony, which cascades into framework upgrades.
  • No auto-discovery, no config publishing. Everything is manual: singleton binding, config entry, env vars. Budget an hour for wiring before your first API call.
  • env() outside config files breaks under config:cache. Always read config('services.crowdin.*'), never env() at runtime.
  • SDK exceptions are still not Laravel-friendly, just different ones. v2.0.0 throws Symfony HttpClient exceptions (TransportExceptionInterface, ClientExceptionInterface, ServerExceptionInterface) plus any Crowdin-specific exceptions the SDK wraps them in. Catch \Throwable at your service boundary — or better, catch HttpExceptionInterface for HTTP failures and \RuntimeException for the rest — and convert to your own exception (or report() + a user-facing message) so callers never see raw HTTP types.
  • RetryableHttpClient retries only by default on connection errors and 5xx. By 2.0.0's design, 4xx will not be retried — correct, because Crowdin 429s are the one you do want to retry. Configure a GenericRetryStrategy with a RetryStrategyInterface-aware decision callback if you want to retry 429s with Retry-After backoff.
  • Testing no longer goes through Guzzle mocks. Any existing test that used HandlerStack/MockHandler needs porting to MockHttpClient/MockResponse. MockResponse can also throw (return a \Throwable from the callback) to simulate transport failures — that's how you exercise your retry/exception paths.
  • Release cadence is the biggest risk. Zero known dependents, 34 stars, and this release is a breaking rewrite of the HTTP layer. Crowdin's API does change; when an endpoint moves you'll be patching the SDK yourself. Pin the exact minor version, and treat vendor/akeneo/crowdin-api as something you may fork. Cross-check behavior against Crowdin's REST docs (v2) rather than trusting the docblocks — especially now, since the HTTP-layer rewrite may have introduced subtle changes to how responses are deserialized.
  • Pagination is manual. Responses are paged; long lists (files, strings, translations) will silently truncate if you only read the first page. Wrap list calls in a loop that follows the offset/limit or Link header until exhausted.
  • Rate limits are real. Crowdin enforces per-token limits. Serial sync jobs with RateLimiter are safer than firing hundreds of parallel requests. With Symfony's client the default is sequential per process, so you're only in danger if you dispatch many parallel jobs — throttle job dispatch, not the client.
  • ID types bite. Crowdin project IDs, file IDs, and storage IDs are strings in URLs but often arrive as ints in responses. Cast explicitly at the boundary.
  • Timeouts and retries belong on the HTTP client. With v2.0.0 this is a first-class concept (timeout / max_duration in the options array of HttpClient::create()), so no more decorator fallback needed — just make sure the SDK actually uses the injected client and doesn't build its own.
  • Debugging: log the x-request-id header Crowdin returns in error responses — support can trace it. Symfony's TraceableHttpClient (enabled in dev via the profiler) replaces Guzzle's debug handler; never enable it in production as it logs full request bodies including tokens.
  • Extension points are thin, but the HTTP client is now swappable. Your extension surface is (a) passing your own HttpClientInterface into the SDK client, (b) subclassing/wrapping the client, (c) decorating your own service. Design your app so a future SDK replacement only touches CrowdinService.
  • Verify against the live API early. Before building a big sync pipeline on top of 2.0.0, run a real read and a real single-file write against a throwaway Crowdin project. The HTTP layer changed; the SDK's response parsing may have shifted with it. That one-hour spike tells you more than any changelog.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core