potsky/microsoft-translator-php-sdk
PHP SDK for the Microsoft Translator API. Wraps the Translator Text and Speech services in a simple client so Laravel and plain PHP apps can translate text, detect languages, and break sentences without handling raw HTTP calls. Lightweight, Composer-installable, and easy to add to existing projects.
composer require potsky/microsoft-translator-php-sdk
composer show potsky/microsoft-translator-php-sdk # confirm the version you actually got
Look at src/ before anything else. The package is a thin wrapper — realistically one client class plus a small auth/HTTP helper — and the README from 2016 is not a reliable description of what the code does today. Two things you need to confirm in the source within the first five minutes:
datamarket.accesscontrol.windows.net/v2/OAuth2-13 with a client ID + client secret (the old Microsoft Translator v2 / Azure DataMarket flow). That endpoint was retired. If that's what's in the source, the package cannot work unmodified and your first task is swapping auth + base URL, not writing features.news up cURL or Guzzle internally with no injection point, you cannot mock it in tests and you cannot point it at a different host without editing vendor code.Credentials. Modern path: create a Translator resource in the Azure portal and note the key, region (e.g. westeurope), and endpoint (https://api.cognitive.microsofttranslator.com). The current v3 API authenticates with an Ocp-Apim-Subscription-Key header — no token dance required.
First use case. Pick the narrowest one: translate a single string and dump the result from php artisan tinker. Don't start with model translation, batching, or a UI. Confirm the request is accepted, then confirm the response shape ([0]['translations'][0]['text'] in v3, a bare string in v2) — the parsing differs and that difference will shape everything downstream.
Bind it once, configure from env. A singleton in a service provider backed by config/translator.php reading TRANSLATOR_KEY, TRANSLATOR_REGION, TRANSLATOR_ENDPOINT keeps credentials out of the codebase and gives you one place to swap client implementations.
$this->app->singleton(Translator::class, fn () => new Translator(
config('translator.key'),
config('translator.region'),
config('translator.endpoint'),
));
Never call it inline in a request. Translation is a 200–800 ms network hop against a rate-limited third party. Put it behind a queued job (TranslateModel, ShouldQueue, $tries = 3, backoff() with growing delays) and translate on save via an observer. The request returns immediately; the translation lands when it lands.
Cache aggressively, keyed on the input. Translations are effectively deterministic for a given (text, from, to, textType). Cache::remember('tr:'.sha1($text.$from.$to.$html), now()->addMonth(), fn () => ...) will typically eliminate the majority of your API calls on a site with repeated strings — nav labels, product categories, boilerplate.
Batch, and chunk. Both API generations offer an array endpoint (v2 TranslateArray, v3 POST /translate with a JSON array of {"Text": ...} objects). One batched call for 50 strings is 50× cheaper in latency and rate-limit budget than 50 calls. Chunk to the documented per-request cap (v2 ≈ 10,000 characters total; v3 ≈ 50,000 characters / 1,000 elements) and split on sentence or paragraph boundaries, never mid-word.
Wrap it in your own interface. Define App\Contracts\Translator with translate(string $text, string $to, ?string $from = null): string and translateMany(array $texts, ...): array, and have the SDK sit behind an adapter. You get a swappable implementation, a fake for tests, and insulation from the package being abandoned — which, at 17 stars and a 2016 last release, it effectively is.
Fall back loudly. On failure return the source string, log the exception with the HTTP status and response body, and increment a counter. A product page rendering English in the French locale is a degraded experience; a 500 is an outage.
Multi-tenant. If different customers have their own Translator keys, don't share the singleton — resolve a fresh client per tenant from a factory and key your cache by tenant, or one customer's quota gets spent on another's traffic.
Testing. Bind a fake Translator implementation in the container and assert your job called it with the right arguments. If you must exercise the real client, point it at a fixture-backed Guzzle MockHandler; do not let CI touch the network.
datamarket.accesscontrol.windows.net was decommissioned; client-ID/secret OAuth against it returns 401/404 regardless of whether your credentials are valid. If the package is hardcoded to it, fork it or bypass its auth entirely and inject a Guzzle client configured with the Ocp-Apim-Subscription-Key header against api.cognitive.microsofttranslator.com/translate?api-version=3.0.^5.0|^6.0. Laravel 9+ ships Guzzle 7. Check composer why-not before you fight the resolver; a 60-line Guzzle client of your own is often less work than forcing the dependency tree.pt-br, zh-Hans, sr-Latn — not pt, zh, sr. This is the single most common source of "the API returned garbage" or a 400.textType=html matters. If you're translating CMS content containing tags, omit it and the API will happily translate your attribute values and mangle markup. Set it explicitly; you can't infer it after the fact, so carry a flag on the field.Retry-After alongside 429/503. Honour it in your job's backoff(); a tight retry loop against a rate limit gets you throttled harder.from whenever you know it. Omitting it triggers auto-detection — an extra cost and an extra failure mode, and it will occasionally misidentify short strings ("Gift" is German as often as English).Ocp-Apim-Subscription-Key into storage/logs. Scrub it in a Monolog processor.CURLOPT_SSL_VERIFYPEER => false. If you find that, fix it before it ships.app/Support/ under your own namespace, or skip it: the current Translator v3 REST surface is a single POST with a JSON body, and a small typed client you control will be more maintainable than an adapter around an abandoned one.How can I help you explore Laravel packages today?