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

Cloud Translate Laravel Package

google/cloud-translate

Idiomatic PHP client for Google Cloud Translation, part of Google Cloud PHP. Integrates translation service programmatically, dynamically translating text between thousands of language pairs. Install via Composer (google/cloud-translate). GA, with authentication guides and API docs.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install

composer require google/cloud-translate

That pulls in google/gax, google/protobuf, and the generated Google\Cloud\Translate\V3 classes. No Laravel service provider is auto-registered — you write a thin one yourself.

2. Authenticate (the step that eats the most time)

  • Create/select a GCP project, enable the Cloud Translation API, and make sure billing is attached (the API returns 403 instantly on bill-less projects).
  • Create a service account with the role Cloud Translation API User, download the JSON key.
  • Point the SDK at it. Application Default Credentials is the supported path:
GOOGLE_APPLICATION_CREDENTIALS=/var/www/app/storage/gcp/translate-sa.json
GOOGLE_CLOUD_PROJECT=my-project

On GKE/Cloud Run/App Engine you don't need the file at all — the metadata server supplies credentials. Locally, gcloud auth application-default login also works.

3. Wire it into Laravel

config/translate.php:

return [
    'project_id' => env('GOOGLE_CLOUD_PROJECT'),
    'location'   => env('GOOGLE_TRANSLATE_LOCATION', 'global'),
];

app/Providers/TranslateServiceProvider.php:

public function register(): void
{
    $this->app->singleton(TranslationServiceClient::class, function () {
        return new TranslationServiceClient([
            'logger' => app(\Psr\Log\LoggerInterface::class),
        ]);
    });
}

4. First use case — translate a single string

use Google\Cloud\Translate\V3\Client\TranslationServiceClient;
use Google\Cloud\Translate\V3\TranslateTextRequest;

$client = app(TranslationServiceClient::class);

$response = $client->translateText(
    (new TranslateTextRequest())
        ->setParent(TranslationServiceClient::locationName(config('translate.project_id'), config('translate.location')))
        ->setContents(['Your invoice is ready.'])
        ->setTargetLanguageCode('de')
        ->setMimeType('text/plain'),
    ['timeoutMillis' => 10000]
);

foreach ($response->getTranslations() as $t) {
    echo $t->getTranslatedText();          // "Ihre Rechnung ist fertig."
    echo $t->getDetectedLanguageCode();   // "en"
}

Where to look first

  • TranslationServiceClient — the only class you call. Tab-complete it in your IDE; every method name maps 1:1 to the REST docs.
  • Google\Cloud\Translate\V3\*Request message classes — request shape lives here, not in the docs site.
  • The official PHP samples: GoogleCloudPlatform/php-docs-samples/tree/master/translate.
  • TranslationServiceClient::locationName() / glossaryName() — use these helpers instead of string-concatenating resource names.

Implementation Patterns

Wrap it in a domain service, not a facade of the SDK

Don't expose TranslationServiceClient to controllers. Expose intent:

final class Translator
{
    public function __construct(private TranslationServiceClient $client) {}

    public function translate(string $text, string $target, ?string $source = null): string
    {
        $key = 'tr:' . md5(implode('|', [$text, $source, $target, config('translate.model')]));

        return Cache::remember($key, now()->addDays(30), function () use ($text, $source, $target) {
            $req = (new TranslateTextRequest())
                ->setParent(TranslationServiceClient::locationName(config('translate.project_id'), config('translate.location')))
                ->setContents([$text])
                ->setTargetLanguageCode($target)
                ->setMimeType('text/plain');

            if ($source) {
                $req->setSourceLanguageCode($source);
            }

            return $this->client->translateText($req)['translations'][0]['translatedText'];
        });
    }
}

Caching is not optional. UI strings, product names, and FAQ answers repeat constantly, and you pay per character.

Batch many strings into one call

One call with 50 contents is dramatically cheaper in latency and quota than 50 calls. The response array is order-preserving, so index-mapping is safe.

$response = $client->translateText(
    (new TranslateTextRequest())
        ->setParent($parent)
        ->setContents(array_values($strings))   // ≤1024 items, ~30k codepoints
        ->setTargetLanguageCode('fr')
        ->setMimeType('text/plain')
);

$translated = array_map(fn ($t) => $t->getTranslatedText(), iterator_to_array($response->getTranslations()));
$mapped = array_combine(array_keys($strings), $translated);

Backfill existing content through the queue

For "translate all 200k existing product descriptions," a Bus::batch() of chunked jobs beats an HTTP request every time. Instantiate the client inside each job (gRPC channels aren't serializable, and queue workers fork).

class TranslateProductChunk implements ShouldQueue
{
    use Batchable, Queueable;

    public $tries = 3;
    public $backoff = [10, 60, 300];

    public function __construct(public array $ids, public string $locale) {}

    public function handle(Translator $translator): void
    {
        Product::whereIn('id', $this->ids)->each(function ($p) use ($translator) {
            $p->setTranslation('description', $this->locale, $translator->translate($p->description, $this->locale, 'en'));
            $p->save();
        });
    }
}

Throttle the batch (->onQueue('translations') + Redis::throttle) so you don't blow the per-minute character quota and get 429s that poison the whole batch.

Glossaries for brand terms and product names

Create the glossary once (it's a long-running operation), store the resource name in config, and attach it to every request so "Laravel", "Stripe", and your SKUs survive translation:

$req->setGlossaryConfig(
    (new TranslateTextGlossaryConfig())
        ->setGlossary(TranslationServiceClient::glossaryName($project, $location, 'brand-terms'))
);

Glossary entries are CSV or TSV files in GCS. Attach per-request — it costs nothing in latency and prevents embarrassing brand mangling.

Language detection before deciding what to do

detectLanguage() is cheap and lets you skip translation entirely for content already in the target language, and pick the right sourceLanguageCode (which improves quality vs. auto-detect for short strings).

$res = $client->detectLanguage(
    (new DetectLanguageRequest())->setParent($parent)->setContent([$text])
);
$lang = $res->getLanguages()[0]->getLanguageCode();

HTML and documents

Set setMimeType('text/html') for markup — the service keeps tags and attributes intact, and only translates text nodes. For uploaded files (PDF/DOCX/PPTX) use translateDocument(), which returns a translated binary stream; for anything batch, use batchTranslateDocument() with GCS input/output.

Model selection

setModel() lets you pick general/nmt (default, cheap, fast) or general/translation-llm for quality-sensitive copy (marketing, support replies). Route per-domain: product descriptions to NMT, customer-facing emails to the LLM model. It's a config toggle, not a code change.

Make it testable

Type-hint your own Translator interface everywhere; in tests bind a fake that returns "[$locale] $text". You should never hit Google in CI. For the one integration test, guard it with if (!env('GOOGLE_APPLICATION_CREDENTIALS')) $this->markTestSkipped().

Offline/localization strings are a build step, not a runtime call

For lang/de.json style app strings, write an artisan translate:sync command that walks missing keys, calls the API in batches, and writes the JSON. Commit the output. Runtime translation of fixed UI copy is slow, costs money per page view, and means a Google outage breaks your navigation bar.

Gotchas and Tips

V2 vs V3 is the #1 source of confusion. Most blog posts and Stack Overflow answers use Google\Cloud\Translate\V2\TranslateClient with translate('Hello', ['target' => 'de']). That surface still exists but is the deprecated legacy API — no glossaries (v2 has them, but a different shape), no AdaptiveMT, no documents, different size limits. Start on V3 and ignore every tutorial that imports TranslateClient.

setParent() must be projects/{id}/locations/{location}. Hand-rolled string concatenation is the most common 400 INVALID_ARGUMENT. Use TranslationServiceClient::locationName(). Use 'global' unless you have a data-residency reason; a wrong location silently routes to a different endpoint and returns 404 on resources (glossaries, datasets) that live elsewhere.

Glossary and dataset resources must be co-located with the request and with each other. A glossary in us-central1 referenced from a global request fails. Decide the location once, put it in config, and never vary it per call.

Batch/document operations need GCS, and need the service account to have bucket access. batchTranslateText() takes gcsSource.inputUri (supports * globs) and gcsDestination.outputUriPrefix, plus mimeType on the input config. Bucket must be in the same region as the request. The 403 you get back says "permission denied" and points at your user, not the SA — check the SA's storage.objectViewer on the bucket.

Batch calls return a long-running operation. Never block a web request on one.

$op = $client->batchTranslateText($request);   // returns Operation
$name = $op->getName();                        // persist this
// later, in a job:
$op = $client->getOperationsClient()->resumeOperation($name, 'batchTranslateText');
if ($op->operationSucceeded()) { ... }

Persist $name in the DB; pollUntilComplete() inside a job with ['pollingIntervalMs' => 10000], not in a controller. Operations expire after ~5 days.

Character accounting is codepoints, not bytes. strlen('こんにちは') is 15; the API counts 5. Use mb_strlen($text, 'UTF-8') for quota math. Emoji count as 2 codepoints and can split awkwardly across batch boundaries — always batch at item boundaries, never by slicing strings.

You will mangle placeholders. Hello :name, you have :count messages comes back as Hallo :name, du hast :count Nachrichten (usually) but not for every language/model. Safest: use HTML <span translate="no"> wrapping (with mimeType = text/html), or translate the template skeleton only and inject variables after. Add a smoke test that asserts every :placeholder survives.

Empty strings, whitespace, and numbers cost money and can error. Filter trim($s) === '', pure-numeric strings, and URLs before sending. Dedupe the array with array_unique() and map back — translation of a 50-item list with 12 duplicates is 24% wasted spend.

Retries: don't hammer, but do retry. 429 RESOURCE_EXHAUSTED and 503 UNAVAILABLE are normal under load. The client has built-in retry config, but bound it:

new TranslationServiceClient([
    'retrySettings' => ['maxRetries' => 3, 'retryDelayMultiplier' => 2.0, 'maxRetryDelayMillis' => 30000],
]);

Then catch and classify:

try { ... } catch (ApiException $e) {
    $e->getStatus();      // 'PERMISSION_DENIED', 'INVALID_ARGUMENT', ...
    $e->getBasicMessage(); // human-readable
    $e->getMetadata();     // request id — paste this into GCP support tickets
}

Retry on 429/503/504; fail fast on 403 and 400 (a retry loop on INVALID_ARGUMENT just burns time).

Credentials inside Laravel bite in three specific places:

  1. php artisan config:cache means env() returns null outside config files. Read config('translate.project_id'), never env().
  2. Queue workers (horizon, queue:work) are long-lived and were started with whatever env existed then — restart them after changing credentials.
  3. Don't use config(['translate.credentials' => json_decode(...)]) with the credentials client option; it's deprecated. Put the JSON on disk, set GOOGLE_APPLICATION_CREDENTIALS in the process env (or use a phpdotenv-free deploy step), and prefer Secret Manager + workload identity in production. Never commit the key; never accept a credentials blob from a user (the upstream README explicitly warns about untrusted credentials).

Timeouts default high. Set ['timeoutMillis' => 10000] as call options on interactive paths; the gRPC default can hang a web request for 60s+ and eat your PHP-FPM workers.

Logging. Pass a PSR-3 logger via the logger client option (Laravel's Log::channel('translation') works — wrap it or resolve Psr\Log\LoggerInterface). You get request/response metadata for free; far better than sprinkling Log::info around.

Testing tip: the SDK isn't mockable via Http::fake() — it's gRPC/HTTP through gax, not Laravel's HTTP client. Fake at your own abstraction boundary. If you must test the SDK itself, use ['credentials' => ...] with a fake and assert the exception path, or point apiEndpoint at a stub server.

Cost control checklist: cache by content hash, batch aggressively, dedupe, skip already-target-language content, use NMT by default and LLM selectively, and never translate in a page-render loop. A "translate on every request" feature is a billing incident waiting to happen.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings