<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 장백호</title>
    <description>The latest articles on DEV Community by 장백호 (@baikho).</description>
    <link>https://dev.to/baikho</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F80404%2F7269c3c4-d264-40e7-90ed-f82ceea3946a.jpeg</url>
      <title>DEV Community: 장백호</title>
      <link>https://dev.to/baikho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/baikho"/>
    <language>en</language>
    <item>
      <title>Stop Writing Custom Importers: Import Multilingual Data in Drupal with Migrate API</title>
      <dc:creator>장백호</dc:creator>
      <pubDate>Sat, 28 Mar 2026 19:36:39 +0000</pubDate>
      <link>https://dev.to/baikho/stop-writing-custom-importers-import-multilingual-data-in-drupal-with-migrate-api-m35</link>
      <guid>https://dev.to/baikho/stop-writing-custom-importers-import-multilingual-data-in-drupal-with-migrate-api-m35</guid>
      <description>&lt;p&gt;Most Drupal developers still write custom importers for external data.&lt;/p&gt;

&lt;p&gt;In many cases, that’s unnecessary. Drupal’s Migrate API already provides a robust and extensible solution, even for complex, multilingual imports.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post is intended for beginner and mid-level Drupal Developers. The examples provided follow the current supported Drupal standards.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you'll learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to import XML feeds using Drupal Migrate API&lt;/li&gt;
&lt;li&gt;How to handle multilingual content in a single migration&lt;/li&gt;
&lt;li&gt;How to keep content in sync with remote systems&lt;/li&gt;
&lt;li&gt;How to automate imports using cron&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;In many enterprise projects, you’ll need to regularly import data from third-party APIs or feeds. Having worked extensively on such integrations, I want to show how you can build a clean, maintainable importer in Drupal without reinventing the wheel.&lt;/p&gt;

&lt;p&gt;This is an example feed of job postings that need importing into the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Fictional HR feed (Publication per locale). Each Publication has an ID attribute. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Jobs&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Job&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1001"&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1001"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ExternalPublication&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Publication&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1001-en_US"&lt;/span&gt; &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"en_US"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Jobname&amp;gt;&lt;/span&gt;Developer Advocate&lt;span class="nt"&gt;&amp;lt;/Jobname&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ShortDescription&amp;gt;&lt;/span&gt;Help developers succeed with our APIs and tools.&lt;span class="nt"&gt;&amp;lt;/ShortDescription&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Leadaftertitle&amp;gt;&lt;/span&gt;Remote-first, documentation-focused team.&lt;span class="nt"&gt;&amp;lt;/Leadaftertitle&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Publicationdate&amp;gt;&lt;/span&gt;2026-01-15&lt;span class="nt"&gt;&amp;lt;/Publicationdate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;URL&amp;gt;&lt;/span&gt;https://www.example.org/jobs/apply/demo-1001&lt;span class="nt"&gt;&amp;lt;/URL&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Publication&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Publication&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1001-nl_NL"&lt;/span&gt; &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"nl_NL"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Jobname&amp;gt;&lt;/span&gt;Developer Advocate&lt;span class="nt"&gt;&amp;lt;/Jobname&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ShortDescription&amp;gt;&lt;/span&gt;Ontwikkelaars helpen slagen met onze API's en tools.&lt;span class="nt"&gt;&amp;lt;/ShortDescription&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Leadaftertitle&amp;gt;&lt;/span&gt;Remote-first, focus op documentatie.&lt;span class="nt"&gt;&amp;lt;/Leadaftertitle&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Publicationdate&amp;gt;&lt;/span&gt;2026-01-15&lt;span class="nt"&gt;&amp;lt;/Publicationdate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;URL&amp;gt;&lt;/span&gt;https://www.example.org/jobs/apply/demo-1001&lt;span class="nt"&gt;&amp;lt;/URL&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Publication&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ExternalPublication&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Job&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Job&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1002"&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1002"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ExternalPublication&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Publication&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1002-en_US"&lt;/span&gt; &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"en_US"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Jobname&amp;gt;&lt;/span&gt;Site Reliability Engineer&lt;span class="nt"&gt;&amp;lt;/Jobname&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ShortDescription&amp;gt;&lt;/span&gt;Keep production calm, observable, and boring—in a good way.&lt;span class="nt"&gt;&amp;lt;/ShortDescription&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Leadaftertitle&amp;gt;&lt;/span&gt;On-call rotation with strong blameless culture.&lt;span class="nt"&gt;&amp;lt;/Leadaftertitle&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Publicationdate&amp;gt;&lt;/span&gt;2026-02-01&lt;span class="nt"&gt;&amp;lt;/Publicationdate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;URL&amp;gt;&lt;/span&gt;https://www.example.org/jobs/apply/demo-1002&lt;span class="nt"&gt;&amp;lt;/URL&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Publication&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ExternalPublication&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Job&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Job&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1003"&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1003"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ExternalPublication&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Publication&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"DEMO-1003-fr_FR"&lt;/span&gt; &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"fr_FR"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Jobname&amp;gt;&lt;/span&gt;Ingénieur logiciel&lt;span class="nt"&gt;&amp;lt;/Jobname&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ShortDescription&amp;gt;&lt;/span&gt;Concevoir et maintenir des services fiables, du prototype à la production.&lt;span class="nt"&gt;&amp;lt;/ShortDescription&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Leadaftertitle&amp;gt;&lt;/span&gt;Équipe produit, stack moderne, télétravail possible.&lt;span class="nt"&gt;&amp;lt;/Leadaftertitle&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Publicationdate&amp;gt;&lt;/span&gt;2026-03-01&lt;span class="nt"&gt;&amp;lt;/Publicationdate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;URL&amp;gt;&lt;/span&gt;https://www.example.org/jobs/apply/demo-1003&lt;span class="nt"&gt;&amp;lt;/URL&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Publication&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ExternalPublication&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Job&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Jobs&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things that immediately come to mind are; How much data? How often? Are incremental updates required? Does data need to be in sync with remote removals? All of these will attribute to the choice of implementation.&lt;/p&gt;

&lt;p&gt;Assuming all these requirements are applicable, we come down to a few solutions in the Drupal landscape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrate API (semi custom - migrate scaffolding - extensible)&lt;/li&gt;
&lt;li&gt;Feeds (click and assemble - interface driven - extensible)&lt;/li&gt;
&lt;li&gt;Fully custom (write from scratch - high maintenance)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Migrate API?
&lt;/h2&gt;

&lt;p&gt;We chose the Migrate API as it lives in core and has matured in various ways alongside many community contributed building blocks and tooling on top. For our client's needs it was the perfect choice to build the importer while being mindful of low effort, high quality and easy maintainability.&lt;/p&gt;

&lt;p&gt;For the custom integration, we will rely on the following stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrate - Core eco system and scaffolding&lt;/li&gt;
&lt;li&gt;Migrate Plus - Extended set of plugins for fetching, parsing, auth, etc...&lt;/li&gt;
&lt;li&gt;Migrate Tools - Extra commands to aid the importer&lt;/li&gt;
&lt;li&gt;Ultimate Cron - Advanced scheduled task administration (optional)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;XML Feed → Migrate Source → Process Plugins → Node &lt;span class="o"&gt;(&lt;/span&gt;translations&lt;span class="o"&gt;)&lt;/span&gt;
                                ↓
                           Cron &lt;span class="o"&gt;(&lt;/span&gt;Ultimate Cron&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;I created a &lt;a href="https://github.com/baikho/drupal-jobs_import_demo" rel="noopener noreferrer"&gt;demo&lt;/a&gt; to support this blog post while expanding on the most notable pieces of its functionality:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Jobs Import Demo&lt;/strong&gt;: &lt;a href="https://github.com/baikho/drupal-jobs_import_demo" rel="noopener noreferrer"&gt;https://github.com/baikho/drupal-jobs_import_demo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Install the demo, by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require baikho/drupal-jobs_import_demo
drush en jobs_import_demo &lt;span class="nt"&gt;-y&lt;/span&gt;
drush cr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We start with the migrate YAML that will map the ETL processes altogether:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jobs&lt;/span&gt;
&lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Jobs&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(demo)'&lt;/span&gt;
&lt;span class="na"&gt;migration_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jobs_import_demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a migration group is not needed, but it will help if you have an advanced migration with many referencing entities of different types.&lt;/p&gt;

&lt;p&gt;Next up is our migration source definition in which we can specify our custom source plugin, and out of the box available fetcher and parser plugins. We also want to make sure we track changes so setting that flag as &lt;code&gt;TRUE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the sake of the demo we use an internal sample endpoint for the http fetcher, but in real-world scenarios this would be a remote URL with an authentication layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;job_feed_url&lt;/span&gt;
  &lt;span class="na"&gt;data_fetcher_plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
  &lt;span class="na"&gt;data_parser_plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple_xml&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
  &lt;span class="na"&gt;track_changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/xml,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text/xml;q=0.9,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/*;q=0.8'&lt;/span&gt;
  &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;item_selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//Job/ExternalPublication/Publication'&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publication_id&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publication&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ID'&lt;/span&gt;
      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@ID'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;job_id&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Job&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ID'&lt;/span&gt;
      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ancestor::Job/@id'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;locale&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publication&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;locale'&lt;/span&gt;
      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@language'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;title&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Title&lt;/span&gt;
      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Jobname'&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we will define our migration index, in which we will use a composite key as the entity ids are uniquely identified by a combination of &lt;code&gt;job_id&lt;/code&gt; + &lt;code&gt;locale&lt;/code&gt; to match Drupal's architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;job_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our data transformation will happen in the process definition. We map the titles and languages. Already we can see some useful plugins at work, like the &lt;code&gt;skip_on_empty&lt;/code&gt; and &lt;code&gt;static_map&lt;/code&gt; plugins. We won't cover all fields, but you can find them in the demo repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skip_on_empty&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;title&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;row&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Missing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;title'&lt;/span&gt;

  &lt;span class="na"&gt;langcode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;static_map&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;locale&lt;/span&gt;
    &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;en_US&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
      &lt;span class="na"&gt;nl_NL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nl&lt;/span&gt;
      &lt;span class="na"&gt;fr_FR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fr&lt;/span&gt;
    &lt;span class="na"&gt;default_value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make multilingual data land as entity translations we write a custom plugin and pipe it upon the result of the &lt;code&gt;migration_lookup&lt;/code&gt; plugin. This will settle our first unique id index as the source translation in sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# Same job_id exists for every locale row; lookup by job_id alone can return&lt;/span&gt;
  &lt;span class="c1"&gt;# several nids. Translation rows need one nid — use first id (same job).&lt;/span&gt;
  &lt;span class="na"&gt;nid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migration_lookup&lt;/span&gt;
      &lt;span class="na"&gt;migration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jobs&lt;/span&gt;
      &lt;span class="na"&gt;source_ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;job_id&lt;/span&gt;
      &lt;span class="na"&gt;no_stub&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migration_lookup_first_nid&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt;
      &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skip_on_empty&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;process&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the demo code for the &lt;code&gt;migration_lookup_first_nid&lt;/code&gt; helper plugin.&lt;/p&gt;

&lt;p&gt;Finally to map the data transformation on the Drupal entity type we add the following destination definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;entity:node'&lt;/span&gt;
  &lt;span class="na"&gt;default_bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vacancy&lt;/span&gt;
  &lt;span class="na"&gt;translations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that our ETL process is final, we can already run this on demand by executing the migrate drush command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;drush mim &lt;span class="nb"&gt;jobs&lt;/span&gt;
 &lt;span class="o"&gt;[&lt;/span&gt;notice] Processed 4 items &lt;span class="o"&gt;(&lt;/span&gt;4 created, 0 updated, 0 failed, 0 ignored&lt;span class="o"&gt;)&lt;/span&gt; - &lt;span class="k"&gt;done &lt;/span&gt;with &lt;span class="s1"&gt;'jobs'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! Now we have our 4 vacancy nodes created from the feed just like that, while respecting entity translations in a single migrate YAML file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0r59afd39y4mnuf98paa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0r59afd39y4mnuf98paa.png" alt=" " width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this were a one-off migration, this would be sufficient and all done! &lt;/p&gt;

&lt;p&gt;However, the client needs this synchronized on an hourly interval. While this would typically be handled via a server-level crontab executing the command, the client preferred to keep this logic within the application itself. We achieved this by creating a small service with a callback for Ultimate Cron and the adjusted command with the &lt;code&gt;--sync&lt;/code&gt; option provided by Migrate Tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Drupal\jobs_import_demo\Service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Cron-style import runner: spawns Drush migrate:import in the background.
 *
 * Ultimate Cron should call {@see self::jobsImportCron()}.
 */&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImportCronService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="cd"&gt;/**
   * Ultimate Cron callback: import jobs for the jobs_import_demo group.
   */&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;jobsImportCron&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jobs_import_demo'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cd"&gt;/**
   * Spawns drush mim in the background (non-blocking).
   *
   * @param string $migrationGroup
   *   Migrate Plus group id (e.g. jobs_import_demo).
   */&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;runGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$migrationGroup&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$drush&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DRUPAL_ROOT&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../vendor/bin/drush'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_executable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$drush&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$drush&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' mim --group='&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$migrationGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' --update --sync &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this extra bit of code in place and the Ultimate Cron config, we can navigate to the Cron interface in Drupal and administer or manually trigger the scheduled task for the import:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i6gaeabik1hiuokgyeg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i6gaeabik1hiuokgyeg.png" alt=" " width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, Migrate API will take care of the remote changes and keep all entities in sync. All of which we didn't have to write ourselves and are able to just solely focus on the business logic mapping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this approach scales
&lt;/h3&gt;

&lt;p&gt;One of the biggest advantages of using Migrate API is that it treats imports as first-class citizens in Drupal.&lt;/p&gt;

&lt;p&gt;Instead of writing one-off scripts, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change tracking&lt;/li&gt;
&lt;li&gt;Rollbacks&lt;/li&gt;
&lt;li&gt;Re-runs&lt;/li&gt;
&lt;li&gt;Extensibility via plugins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it especially suitable for long-lived enterprise integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The implementation has demonstrated how quickly one can build a clean solution in Drupal with minimal effort. It has also proven how good it works with Drupal entities and translations tracking upstream changes.&lt;/p&gt;

&lt;p&gt;You can find the &lt;a href="https://github.com/baikho/drupal-jobs_import_demo" rel="noopener noreferrer"&gt;demo&lt;/a&gt; on my GitHub profile, which is easily installable as a module. Instructions are found in the &lt;a href="https://github.com/baikho/drupal-jobs_import_demo#drupal-jobs-import-demo" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;I want to thank all of the core maintainers and contributors who work actively on enriching Drupal.&lt;/p&gt;

&lt;p&gt;The Drupal ecosystem thrives on community and contributions, so if you want to help it grow make sure to look at the issue queue and start reviewing or testing bugfixes and features.&lt;/p&gt;

&lt;p&gt;In another blog post I will expand on building similar functionality with &lt;strong&gt;Feeds&lt;/strong&gt; which also offers rich tooling, just to show you how diverse the landscape in Drupal really is with various solutions and approaches.&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
