blog/magento-naar-google-shopping-met-vertex-ai.mdx
Alle notities

30 januari 2026

Van Magento naar Google Shopping in vier talen, in één nacht: de pipeline die ik echt heb gebouwd

Vrijwel elk multi-language Google Shopping-account dat ik heb geërfd, brak op een van drie plekken: de feed-export, de vertaalstap of de Merchant Center-upload. Ik bouwde de pipeline die ik zelf wilde: Magento als bron, Cloud Storage als tussenstation, één Cloud Function voor de transform, Vertex AI Translation voor de vertalingen naar de doeltalen, en de Content API om elke nacht vier locales naar Merchant Center te pushen. Architectuur, de code die ertoe doet, de kostenrealiteit, en de ene aanpassing die de vertaalfactuur met een factor tien verlaagde. Plus: voor wie dit nu de moeite waard is, en voor wie nog niet.

Ik heb de afgelopen jaren meer multi-language Google Shopping-accounts geërfd dan ik zou willen toegeven, en ze waren vrijwel allemaal op een van dezelfde drie plekken gebroken, meestal ook nog in dezelfde volgorde. De feed-export is broos, waardoor de catalogusdata incompleet of verouderd binnenkomt. De vertaalstap is er achteraf aangeplakt als handmatig klusje, waardoor de Duitse en Franse copy weken achterloopt op de Nederlandse bron. En de Merchant Center-upload draait op één persoon die dinsdagochtend door de UI klikt. Deze keer bouwde ik alles vanaf nul op Google Cloud voor een EU-catalogus, en ik wilde dat geen van die drie punten stilletjes kon falen.

Waar ik uiteindelijk op uitkwam: één nachtelijke pipeline. Magento exporteert de catalogus. Cloud Storage vangt die op als tussenstation, in het vak heet dat een staging-laag. Die upload zet een berichtje op Pub/Sub, het interne berichtensysteem van Google Cloud, en dat bericht start een Cloud Function: een klein stuk code dat Google voor je draait zonder dat je zelf een server beheert. De function leest de feed, roept Vertex AI Translation aan voor de drie doeltalen en pusht vier feeds naar Google Merchant Center, het productplatform achter Google Shopping (GMC), via de Content API, de koppeling waarmee je producten programmatisch aanlevert in plaats van via de UI. Drie doellanden plus het bronland, één cron, één bucket, één function, vier productdashboards in GMC de volgende ochtend. De hele pipeline draait in minder dan 15 minuten voor een catalogus van 10.000 producten en kost veel minder dan ik verwachtte, zodra ik één specifieke aanpassing had doorgevoerd waar ik aan het einde van dit artikel op terugkom.

Dezelfde drie breekpunten, maar dan afgedekt:

BreekpuntHandmatige opzetDeze pipeline
Feed-exportCSV-exports die uit de pas lopen met de catalogusREST-export per cron, met timestamp en audittrail
VertalingAchteraf, handmatig, weken achterstand op de bronDraait elke nacht automatisch mee, alleen voor gewijzigde producten
Upload naar Merchant CenterEén persoon klikt door de UIContent API pusht vier locales op een vast schema
FoutdetectieIemand merkt het als de omzet daaltAlert zodra het foutpercentage boven nul komt

Architecture diagram, Magento 2 with REST and cron at 01:00 daily on the left, arrow into Cloud Storage staging bucket with 30-day lifecycle, arrow into Pub/Sub feed-uploaded topic listening on OBJECT_FINALIZE, arrow into a Cloud Function gen2 Python box that contains feed parser, translator and gmc uploader modules with memory 1Gi timeout 540s, with Vertex AI Translation v3 and Secret Manager as sidecars, and a bottom row of four GMC feed cards, products-nl source-of-truth, products-en-gb translated, products-de translated, products-fr translated

De architectuur in één alinea

De pipeline bestaat uit zeven Google Cloud-diensten die elk één ding doen. Cloud Scheduler start de run om 01:00 Amsterdamse tijd. Magento ontvangt de API-aanroep en exporteert een GMC-compatibele XML-feed, waarna een Magento cron-job hem uploadt naar een Cloud Storage bucket genaamd raw-feeds/. De upload triggert een Pub/Sub-event op het feed-uploaded topic. Dat event triggert mijn Cloud Function, die de XML parseert naar een lijst met product-dicts, Vertex AI aanroept om de title- en description-velden naar Engels, Duits en Frans te vertalen, en het resultaat via de Content API for Shopping naar Merchant Center pusht. Secret Manager bewaart de GMC-credentials zodat er geen service-account JSON in de function rondslingert. Cloud Logging legt alles vast, en Cloud Monitoring stuurt me een melding als het foutpercentage boven nul uitkomt.

Al het andere in dit artikel werkt die ene alinea verder uit.

Laag 1: de Magento-export

De catalogus is de single source of truth. Elke vertaling, elke Merchant Center-rij, elk analytics-dashboard verderop in de keten verwijst hiernaar terug, dus ik had een export nodig die ik kon vertrouwen. Er zijn drie redelijke manieren om een feed uit Magento 2 te krijgen: de ingebouwde CSV-export, de REST API, en een feed-extensie. Ik koos voor de REST API, omdat de cron-aangestuurde CSV-export uit de pas gaat lopen zodra attributen wijzigen, en de betaalde extensies een ondoorzichtige laag toevoegen die ik niet kan debuggen om 02:00 's nachts, wanneer een feed faalt.

Aan de Magento-kant is het een kleine custom module. Eén cron-job roept /rest/V1/products aan met paginering, normaliseert de afwijkingen in de attribuutset over de catalogus, schrijft een GMC-compatibel XML-bestand naar var/export/products_feed.xml, en uploadt dit naar Cloud Storage met de officiële Google Cloud PHP SDK. De cron draait om 01:00 Amsterdamse tijd en de upload is rond 01:05 klaar bij een catalogus van 10k. De bestandsnaam bevat een timestamp, zodat ik een audittrail heb van elke run die ooit heeft plaatsgevonden.

Wat ik in deze stap zeker niet zou overslaan: de GMC-schemadiscipline op het moment van export. De feed bevat al g:id, g:title, g:description, g:link, g:image_link, g:price, g:availability, g:brand, g:gtin, g:condition, en g:shipping. Als ik dit verderop in de Cloud Function had moeten opschonen, was ik dubbel zoveel tijd kwijt geweest aan debuggen. Bouw de juiste vorm al bij de bron.

Laag 2: de Google Cloud-setup

Project, API's, bucket, service account, Pub/Sub, secrets. In die volgorde, en gescript met gcloud, zodat ik de hele fundering in 10 minuten kan herbouwen zodra ik dit voor een andere klant doe. De API's die ik aanzette waren Storage, Cloud Functions, Cloud Run, Pub/Sub, Cloud Scheduler, Translation, Vertex AI Platform, Secret Manager, Logging en Monitoring. Het enige service account, feed-pipeline-sa, krijgt roles/storage.objectAdmin, roles/aiplatform.user en roles/secretmanager.secretAccessor. Geen extra rollen, geen servicebrede adminrechten, geen JSON-key die ergens in code rondzwerft. Het least-privilege-principe is goedkoop om in één keer op te zetten en duur om later alsnog te implementeren.

De twee buckets zijn raw-feeds en processed-feeds, beide in europe-west4, beide met uniform bucket-level access. Een lifecycle-regel van 30 dagen op de raw bucket verwijdert oude feeds automatisch, zodat de bucket nooit groter wordt dan een paar gigabyte. Een GCS-notificatie op de raw bucket triggert het feed-uploaded Pub/Sub-topic bij OBJECT_FINALIZE, en dat triggert de function.

Secret Manager bewaart de twee waarden die ik niet in code wil hebben: het GMC merchant ID en het JSON service account dat API-toegang tot Merchant Center heeft. De function leest ze uit bij elke run via secretmanager.SecretManagerServiceClient. Een van beide secrets roteren is één gcloud secrets versions add-commando, en de function pakt bij de volgende invocatie automatisch de nieuwe versie op.

Laag 3: de Cloud Function

Eén function, drie Python-bestanden plus een klein main.py entrypoint. feed_parser.py downloadt de XML uit Cloud Storage en parseert hem met lxml naar een lijst met product-dicts. translator.py wrapt de Vertex AI Translation-client, neemt een lijst producten en een doeltaal, en geeft een nieuwe lijst terug met vertaalde title en description. gmc_uploader.py wrapt de Content API, neemt een lijst producten en post ze in batches van 1000 via products().custombatch().

De function draait als gen2-deployment, Python 3.12, met 1 GiB aan geheugen, een timeout van 540 seconden en een maximum van 5 instances. De timeout is het belangrijkste getal: de vertaalstap voor 10k producten kost een paar minuten en de Content API-batches zijn traag, dus de standaard 60 seconden is niet genoeg. De instance cap beschermt me tegen een op hol geslagen recursieve trigger, mocht ik ooit per ongeluk vanuit de function naar dezelfde bucket uploaden.

Het entrypoint leest het Pub/Sub-event, downloadt en parset de feed, en uploadt eerst direct de Nederlandse source-of-truth naar GMC (zodat de NL-feed altijd binnenkomt, ook als de vertaling faalt). Daarna loopt het door de drie doeltalen: het roept Vertex AI per taal aan, zet de juiste contentLanguage en targetCountry op elk product, en uploadt elke vertaalde feed achtereenvolgens. De logs aan het einde van een run bestaan uit vijf gestructureerde entries: NL-aantal geüpload, EN-aantal geüpload, DE-aantal geüpload, FR-aantal geüpload, totale duur. Al het andere is een probleem en triggert een alert.

Laag 4: de vertaalengine kiezen

Vertex AI biedt twee wegen om te vertalen, en het zijn geen gelijkwaardige producten. De basis translate_v3 API is de snelle, goedkope, deterministische optie. Gemini, aangeroepen via hetzelfde Vertex AI-platform maar met het generatieve model, begrijpt veel meer context: het weet dat een safety shoe in het Duits een Sicherheitsschuh is, en niet de letterlijke woord-voor-woordvertaling daarvan. Allebei hebben ze hun eigen toepassing. De verkeerde optie kiezen kost óf geld óf copykwaliteit, en je herstelt dat niet zonder de hele catalogus opnieuw te draaien.

Side-by-side comparison panel, left card on Cloud Translation API v3 listing best-for cases as large catalogs and batch translation, cost ~20 USD per million chars which works out to roughly 400 EUR per month for 10k products times 4 languages, characteristics including deterministic outputs and glossary support, limitations being literal translation and stiff CTAs, right card on Gemini 1.5 Flash on Vertex AI listing best-for cases as titles only on top SKUs and long-form marketing descriptions, cost ~0.10 USD per million input tokens which is far cheaper for short content, characteristics including context-aware translation and tone preservation, limitations being non-deterministic outputs requiring a prompt eval loop

Waar ik op uitkwam: de Translation API gebruiken als standaard voor de hele catalogus, omdat deterministisch en goedkoop op schaal de doorslag geeft bij 10.000 productomschrijvingen die niemand regel voor regel leest. Voor de top 200 SKU's (artikelnummers) op omzet draai ik een tweede pass met Gemini, alleen op de titel en de eerste 200 tekens van de omschrijving. Het kostenverschil is verwaarloosbaar: het verschil in marketingtoon is het verschil tussen een Duitse titel die converteert en een titel die als vertaling leest. De 9.800 SKU's in de long tail kunnen prima met de basisvertaling, en het auditlog op de function houdt bij welke SKU's welke behandeling kregen, zodat ik de Gemini-pass kan uitbreiden als de data laat zien dat het zich terugbetaalt.

Het andere onderdeel dat ik nooit zou overslaan: een glossary, een vaste woordenlijst die de vertaal-API verplicht volgt. De catalogus staat vol merknamen, modelcodes en technische certificeringen die nooit vertaald mogen worden. Een glossary-CSV bouwen met source_term,target_term,source_language_code,target_language_code en die uploaden naar de Translation API via glossaries.create kost eenmalig 20 minuten en voorkomt precies het soort fout waardoor een Duitse prospect het tabblad sluit. Snickers Workwear is niet Snickers Arbeitskleidung.

Laag 5: de Merchant Center-koppeling

Vier feeds in Merchant Center, één per locale. products-nl voor de Nederlandse source-of-truth catalogus, gericht op Nederland, products-en-gb voor de Engelse vertaling gericht op het Verenigd Koninkrijk, products-de voor Duitsland, products-fr voor Frankrijk. Elke feed heeft zijn eigen diagnostics-tab in GMC, dus wanneer er iets breekt, zie ik meteen in welke taal het breekt. Elke feed heeft zijn eigen contentLanguage en targetCountry, per item ingesteld via de Content API, wat de schoonste mapping is voor multi-locale Merchant Center-opzetten.

Ik koos voor de Content API boven de URL-gebaseerde feed om één specifieke reden. URL-feeds worden door Merchant Center opgehaald op zijn eigen schema, wat betekent dat ik geen controle heb over wanneer de vernieuwing landt. De Content API pusht de producten elke nacht op mijn schema, dus ik weet precies wanneer de catalogus in Merchant Center overeenkomt met de catalogus in Magento. De andere reden is doorvoer: het custombatch-endpoint verwerkt 1000 producten tegelijk, dus 10.000 producten gaan er in 10 batches per taal doorheen, wat comfortabel binnen de function-timeout past.

Eén ontwikkeling om mee te nemen als je dit vandaag nabouwt: Google is de Content API for Shopping aan het uitfaseren ten gunste van de nieuwere Merchant API. Bestaande integraties draaien op dit moment gewoon door, maar de richting is onmiskenbaar. Voor deze architectuur maakt dat weinig uit: de nachtelijke batch, de vier feeds en de per-item locale-mapping blijven precies hetzelfde, alleen de uploader-module praat straks tegen een ander endpoint. Begin je nu vanaf nul, bouw gmc_uploader.py dan direct op de Merchant API, dan hoef je die migratie niet meer in te plannen.

De ene valkuil waar ik in trapte was de mime_type-parameter van de Translation API. De standaard is plain text. Zet deze op text/html als jouw omschrijvingen inline HTML bevatten, anders worden <br/>-tags eruit gestript en wordt jouw Franse omschrijving een muur van ongeformatteerde tekst. De Franse content ging als laatste door mijn pipeline omdat de omschrijvingen gemiddeld langer zijn, en dat was de locale waar ik het opmaakprobleem voor het eerst opmerkte. Door mime_type=text/html op requestniveau in te stellen, loste ik het voor alle vier de locales in één keer op.

De nachtelijke timing

Het cron-schema is zo gebouwd dat er tussen elke stap ruimte zit, voor het geval er iets uitloopt. Om 01:00 start Cloud Scheduler Magento. Om 01:05 uploadt de Magento cron de XML naar GCS. Om 01:10 stuurt Pub/Sub het event en pakt de Cloud Function het bestand op. Om 01:15 is de function klaar met parsen en begint de vertaling. Om 02:00 heeft hij alle vier de locales naar GMC geüpload. Om 04:00 zit de veiligheidsmarge: als er nog URL-gebaseerde feeds in GMC staan (ik houd er één aan voor legacy-doeleinden), worden die dan opgehaald. Om 05:00 kloppen alle dashboards die ik 's ochtends bekijk met elkaar.

Ik heb dit bewust niet event-driven opgezet vanuit Magento product-save events. Een drukbezochte catalogus produceert tijdens piekuren tientallen product-save events per minuut. Dat betekent tientallen GMC-uploads per minuut, en dus is het dagelijkse Merchant Center-quotum al voor de lunch verbruikt. De nachtelijke batch is eerlijk: hij kost me vernieuwingssnelheid ten opzichte van realtime, in ruil voor voorspelbaarheid en een quotum dat nooit opraakt.

De kostenrealiteit

Hier had ik een verrassing verwacht, en die kreeg ik ook, alleen niet in de richting die ik had voorzien. De opzetkosten voor Cloud Storage, Cloud Functions, Pub/Sub, Scheduler en Secret Manager tellen samen op tot minder dan 5 EUR per maand. Vrijwel de hele rekening is de vertaalstap.

Bar chart of monthly cost for 10,000 product catalog, baseline scenario translating the full catalog every night, Vertex AI Translation roughly 400 EUR per month dominates the chart while Cloud Storage and Cloud Functions and Scheduler and Secret Manager are all under 1 EUR each, total 400 EUR per month, then a second smaller chart showing the same pipeline with delta-only translation dropping the Vertex AI bill from 400 EUR to roughly 30 EUR per month, with a note that storing last_translated_at per SKU in Firestore or BigQuery and skipping unchanged products is the one optimisation that pays back

De Translation API rekent op het moment van schrijven ongeveer 20 USD per miljoen tekens. Een typisch e-commerceproduct heeft ruwweg 500 tekens over titel en omschrijving samen, dus 10.000 producten is ongeveer 5 miljoen tekens per taal. Vermenigvuldigd met drie doeltalen komt dat neer op 15 miljoen tekens per nachtelijke run, oftewel ongeveer 13 EUR per nacht, wat oploopt tot ongeveer 400 EUR per maand als je elke nacht de hele catalogus opnieuw vertaalt.

De meeste catalogi veranderen niet elke dag. De mijne al helemaal niet: een gemiddelde dag raakt een paar honderd SKU's, niet 10.000. Dus de ene optimalisatie die zich onmiddellijk en zichtbaar terugbetaalt: alleen de deltas vertalen. Sla een last_translated_at-timestamp per SKU per taal op, in Cloud Firestore of BigQuery, en haal bij de start van elke run alleen de producten op waarvan de updated_at in Magento nieuwer is dan hun laatste vertaling. Bij een typische catalogus verlaagt dit het vertaalvolume met 90 tot 95 procent, en daalt de maandfactuur van 400 EUR naar ongeveer 30 EUR. De rest van de pipeline blijft ongewijzigd.

Het kostte me ongeveer een dag om de delta-logica toe te voegen. Van alle code in dit project heeft dit stukje het hoogste rendement per bestede minuut.

Wat ik zou overslaan als ik dit opnieuw zou bouwen

  • De processed-feeds bucket. Ik voegde hem toe in de veronderstelling dat ik de vertaalde output zou willen archiveren voor debugging. In de praktijk geeft de Content API me de diagnostiek die ik nodig heb, en staat de bucket meestal leeg. Hem weghalen scheelt lifecycle-complexiteit.
  • URL-feeds als fallback. Ik hield er één aan voor de zekerheid. Die is in productie precies nul keer geactiveerd. De Content API is betrouwbaarder gebleken dan ik verwachtte.
  • De Magento product-save webhook. Ik heb erover nagedacht, ertegen besloten en er nooit spijt van gehad. De nachtelijke batch is genoeg voor Shopping.
  • Elk veld vertalen. Titel en omschrijving zijn 90 procent van de marketingwaarde. Merk, GTIN, conditie, beschikbaarheid en verzending zouden nooit vertaald moeten worden. Ik heb de function beperkt tot die twee velden en de catalogus leest correct in alle vier de locales.

Wat ik niet zou veranderen

  • Eén service account, nauw afgebakend. De pipeline heeft niets meer nodig, en IAM-wildgroei is het soort rommel waar je pas achter komt als je probeert het project over te dragen.
  • Secrets in Secret Manager, niet in code. Kost niets, elimineert een hele categorie incidenten uit het dreigingsmodel.
  • Translation API voor de long tail, Gemini voor de top-SKU's. Zowel de kosten als de kwaliteit betalen zich terug. Kiezen voor het een óf het ander is de verkeerde aanpak.
  • Een glossary voor merk- en modelnamen. De goedkoopste taak van 20 minuten met de grootste impact in het hele project.
  • De delta-only vertaallogica. Hierboven al besproken. Zonder deze logica is de hele pipeline te duur om te rechtvaardigen. Mét deze logica is de rekening een afrondingsverschil ten opzichte van wat de multi-locale Shopping-omzet oplevert.

Voor wie dit de moeite waard is, en voor wie nog niet

Dit is interessant voor webshops die al in meerdere EU-landen verkopen, of daar concreet naartoe werken, met een catalogus vanaf pakweg duizend producten. Op die schaal is handmatig bijhouden geen serieuze optie meer: de Duitse copy loopt structureel achter, niemand kijkt de feed-diagnostiek per land na, en het hele proces hangt op één persoon. De rekensom is dan simpel. Als iemand in het team nu elke week uren kwijt is aan feeds en vertalingen, kan een pipeline van een paar dagen bouwwerk zich binnen enkele maanden terugverdienen, nog los van wat de extra locales aan Shopping-omzet opleveren.

Voor wie dit nog niet is: verkoop je in één land en heb je geen concrete uitbreidingsplannen, dan lost deze pipeline een probleem op dat je niet hebt. Een catalogus van een paar honderd producten kan prima toe met een feed-extensie en een vertaalronde per kwartaal. En de eerlijke realiteit die ik bij veel Nederlandse webshops zie: de productdata in Magento is zelf niet op orde. Attributen die per categorie anders heten, ontbrekende GTIN's, omschrijvingen die nooit zijn afgemaakt. Dan is de eerste stap geen pipeline maar datakwaliteit, want een automatische vertaling van een slechte omschrijving is een slechte omschrijving in vier talen.

De realistische eerste stap is dan ook niet de volledige architectuur uit dit artikel. Begin met een betrouwbare export en één doeltaal, kijk wat de vertaalkwaliteit en de Shopping-resultaten in dat land doen, en bouw daarna pas de delta-logica en de overige locales erbij. Elke laag hierboven werkt ook los van de rest.

Waar ik op uitkom

De vorm van deze pipeline is dezelfde die ik voor elke klant met een EU multi-locale Shopping-footprint en een catalogus van meer dan een paar honderd SKU's zou bouwen. De fout die ik het vaakst zie: teams plakken de vertaling er achteraf als apart handmatig proces bij. Dat levert een vernieuwingsgat op tussen de brontaal en de rest, en een workflow die breekt zodra iemand op vakantie gaat. De andere fout die ik geregeld tegenkom: elke nacht de hele catalogus laten vertalen zonder te checken hoeveel er daadwerkelijk is veranderd. De versie die werkt behandelt Magento, Cloud Storage, de Cloud Function, Vertex AI en Merchant Center als één nachtelijke batch, met één service account, één bucket, één trigger en een deltafilter dat de vertaalrekening eerlijk houdt.

Zit je met een Magento-catalogus en Google Shopping in slechts één land, en wil je een tweede paar ogen op wat er nodig is om hier een vierlocale versie van te maken? Stuur me een bericht. Mail info@jermayads.nl of gebruik jermayads.nl/contact.