3

I'm migrating multilingual content to Drupal 8 from a group of CSV files, and I'm about 95% of the way there. I have an image field attached to an article content type that shares the image among all translations, but the alt attribute is translatable. The remaining 5% problem is in configuring the migration to use a single common image file fid for all languages but a different alt text for other languages.

articles.csv

id,title_en,title_fr,body_en,body_fr,image,alt_en,alt_fr
1,Title 1,Titre 1,<p>English Body paragraph text 1</p>,<p>French Body paragraph text 1</p>,image1.jpg,Alt text English 1,Alt text French 1
2,Title 2,Titre 2,<p>English Body paragraph text 2</p>,<p>French Body paragraph text 1</p>,image2.jpg,Alt text English 2,Alt text French 2
3,Title 3,Titre 3,<p>English Body paragraph text 3</p>,<p>French Body paragraph text 1</p>,image3.jpg,Alt text English 3,Alt text French 3

images.csv

image
image1.jpg
image2.jpg
image3.jpg

English Content Migration w/ Image & Alt text

My default English content migration works great with images being imported and attached to nodes.

migrate_plus.migration.articles_images.yml

id: articles_images
status: true
dependencies:
  module:
    - file
migration_tags:
  - CSV
migration_group: default
label: Article Images
source:
  constants:
    source_base_path: ../import/images
    uri_file: 'public://images/imported'
  plugin: csv
  track_changes: true
  path: ../import/images.csv
  header_row_count: 1
  keys:
    - image
  column_names:
    -
      image: Image
process:
  source_full_path:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/source_base_path
        - image
    -
      plugin: urlencode
  uri_file:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/uri_file
        - image
    -
      plugin: urlencode
  filename:
    -
      plugin: skip_on_empty
      method: row
      source: image
    -
      plugin: get
      source: image
  uri:
    plugin: file_copy
    source:
      - '@source_full_path'
      - '@uri_file'
destination:
  plugin: 'entity:file'
migration_dependencies:
  required: {  }
  optional: {  }

migrate_plus.migration.articles_en.yml

id: articles_en
status: true
migration_tags:
  - CSV
migration_group: default
label: Articles
source:
  plugin: csv
  path: ../import/articles.csv
  header_row_count: 1
  keys:
    - id
  column_names:
    -
      id: Id
    -
      title_en: 'Title EN'
    -
      title_fr: 'Titre FR'
    -
      body_en: 'Text EN'
    -
      body_fr: 'Texte FR'
    -
      image: Image
    -
      alt_en: 'Alt Text EN'
    -
      alt_fr: 'Alt Text FR'
process:
  type:
    plugin: default_value
    default_value: article
  title: title_en
  body/value: body_en
  body/format:
    plugin: default_value
    default_value: basic_html
  field_image/target_id:
    -
      plugin: skip_on_empty
      method: process
      source: image
    - 
      plugin: migration
      migration: articles_images
      source: image
      no_stub: true
  field_image/alt:
    -
      plugin: skip_on_empty
      method: process
      source: image
    -
      plugin: skip_on_empty
      method: process
      source: alt_en
    -
      plugin: get
      source: alt_en
destination:
  plugin: 'entity:node'
migration_dependencies:
  optional:
    - articles_images

French Content Translation w/ different alt text

However, every configuration I try to make the foreign language translations use the same FID but a different alt text fails.

migrate_plus.migration.articles_fr.yml

id: articles_fr
status: true
migration_tags:
  - CSV
migration_group: default
label: Articles French
source:
  plugin: csv
  path: ../import/articles.csv
  header_row_count: 1
  keys:
    - id
  column_names:
    -
      id: Id
    -
      title_en: 'Title EN'
    -
      title_fr: 'Titre FR'
    -
      body_en: 'Text EN'
    -
      body_fr: 'Texte FR'
    -
      image: Image
    -
      alt_en: 'Alt Text EN'
    -
      alt_fr: 'Alt Text FR'
process:
  type:
    plugin: default_value
    default_value: article
  nid:
    plugin: migration
    source: id
    migration: articles_en
  langcode:
    plugin: default_value
    default_value: fr
  title:
    -
      plugin: skip_on_empty
      method: row
      source: title_fr
    -
      plugin: get
      source: title_fr
      language: fr
  body/value:
    -
      plugin: skip_on_empty
      method: row
      source: body_fr
    -
      plugin: get
      source: body_fr
      language: fr
  body/format:
    plugin: default_value
    default_value: basic_html
  field_image/target_id:
    plugin: migration
    migration: articles_images
    source: image
    language: fr
  field_image/alt:
    -
      plugin: skip_on_empty
      method: process
      source: image
    -
      plugin: skip_on_empty
      method: process
      source: alt_fr
    -
      plugin: get
      source: alt_fr
      language: fr
destination:
  plugin: 'entity:node'
  translations: true
migration_dependencies:
  required:
    - articles_en
  optional: {  }

Running the French data migration, I get an error message.

InvalidArgumentException: Invalid translation language (und) specified.
in Drupal\Core\Entity\ContentEntityBase->getTranslation() (line 745 of
web/core/lib/Drupal/Core/Entity/ContentEntityBase.php).
Invalid translation language (und) specified.
(web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php:770)

French Content Translation w/ basic image sharing

I've also tried processing just the field_image to copy over to the the foreign translations, but it gives the same error:

field_image:
  plugin: migration
  migration: articles_images
  source: image
  no_stub: true

Adding or leaving off the language: fr parameter from any of the field processors seems to make no difference -- the error is the same either way.

The intended resulting data structure

Inspecting a field_image attached to a node that was created and translated manually through Drupal UI at node/X/devel shows the following structure:

[field_image] => array(
  [x-default] => array(
    [0] => array(
      [target_id] => 123
      [alt] => English Alt text
      [title] => 
      [width] => 480
      [height] => 360
    )
  )
  [fr] => array(
    [0] => array(
      [target_id] => 123
      [alt] => French alt text.
      [title] => 
      [width] => 480
      [height] => 360
    )
  )
)

Additional Resources

These blog posts and example repositories have been helpful in the lead up to where I'm currently stuck:

  • Can you post a few lines of your cvs file? Just to test the migrations – Adrian Cid Almaguer Feb 27 '17 at 3:29
  • 1
    @AdrianCidAlmaguer complete CSV and yaml files have been added. – JamesWilson Feb 27 '17 at 13:58
  • migration: article_images looks like a typo. That should be migration: articles_images, right? – vasi Feb 27 '17 at 15:49
  • thanks, typo fixed. (sadly had nothing to do with the bug, just a copy/paste error in my generalizations from project-specific code names to this) – JamesWilson Feb 27 '17 at 19:26
4

I think I've figured this out. You need to define the 'content_translation_source' property on the translated node, so that Drupal knows what language it's coming from.

Eg:

migrate_plus.migration.articles_fr.yml

process:
  #...
  content_translation_source:
    plugin: default_value
    default_value: en
  #...

(Be sure to substitute the value en for whatever language you've first imported content -- the example in the OP uses English, but this might be anything).


I was asked how I figured this out:

I'm the guy who got multilingual migrations into D8 core, so I do know a bit about the matter :)

In this case, I used xdebug and put a breakpoint where the exception was thrown (ContentEntityBase.php line 745). It looked weird that it was using 'und' as the langcode, rather than 'en'—so I looked up the call stack to see where that was coming from. It turns out to be from content_translation_entity_presave(), which runs:

$source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;

In this case $manager->getTranslationMetadata($entity)->getSource() is basically equivalent to $entity->content_translation_source->value.

I wondered, why would that yield 'und'? I checked what happens when I translate an entity in the browser, and in that case it gives me 'en'. Something must be setting the source language! Indeed, ContentTranslationHandler::entityFormEntityBuild() calls setSource() when we're translating in-browser.

So, I figured I would just replicate that behaviour in the migration, by setting content_translation_source in the process. This also explains why D6 -> D8 migration of translations work, since d6_node_translation.yml has content_translation_source in the process.

  • Thanks @vasi and welcome to Stack Exchange. I've edited your comment to make the example a bit more clear (their flavor of markdown doesn't format code blocks well with three backticks). I will test this out ASAP and confirm to see if it works. – JamesWilson Feb 28 '17 at 22:28
  • Oops. I've deleted my previous comment, due to my own error (my source language wasn't "en". This content_translation_source fixed the issue! Amazing work Vasi++ THANK YOU. – JamesWilson Mar 1 '17 at 12:43
  • I'm curious how you figured this out? – JamesWilson Mar 1 '17 at 12:47
  • 1
    I added some debugging explanation to the answer, I hope that's helpful. – vasi Mar 2 '17 at 0:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.