diff --git a/composer.json b/composer.json
index f8c79040962f39ad372fcc2ffe805bcf9f22e8aa..5b83184f19f5585adc5b278ee80b7fb19edf8e9a 100644
--- a/composer.json
+++ b/composer.json
@@ -10,7 +10,7 @@
         "cweagans/composer-patches": "^1.7",
         "drupal/core": "^9.2",
         "drupal/twig_field_value": "^2.0",
-        "openeuropa/oe_bootstrap_theme": "0.1.202203290731"
+        "openeuropa/oe_bootstrap_theme": "0.1.202204061107"
     },
     "require-dev": {
         "composer/installers": "^1.11",
@@ -59,6 +59,17 @@
             "url": "https://github.com/openeuropa/oe_starter_content"
         }
     },
+    "autoload": {
+        "psr-4": {
+            "Drupal\\oe_whitelabel\\": "./src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Drupal\\Tests\\oe_whitelabel\\": "./tests/src/",
+            "Drupal\\Tests\\oe_bootstrap_theme\\": "./build/themes/contrib/oe_bootstrap_theme/tests/src/"
+        }
+    },
     "extra": {
         "composer-exit-on-patch-failure": true,
         "enable-patching": true,
@@ -71,8 +82,8 @@
             }
         },
         "patches": {
-            "openeuropa/oe_bootstrap_theme" : {
-                "1.x latest": "https://github.com/openeuropa/oe_bootstrap_theme/compare/0.1.202203290731..1.x.diff"
+            "openeuropa/oe_paragraphs": {
+                "latest": "https://github.com/openeuropa/oe_paragraphs/compare/1.12.0..master.diff"
             }
         },
         "drupal-scaffold": {
diff --git a/modules/oe_whitelabel_helper/src/EuropeanUnionLanguages.php b/modules/oe_whitelabel_helper/src/EuropeanUnionLanguages.php
deleted file mode 100644
index 9ac60d58512bcf24f145222c85cf20adc9fdbdd4..0000000000000000000000000000000000000000
--- a/modules/oe_whitelabel_helper/src/EuropeanUnionLanguages.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\oe_whitelabel_helper;
-
-/**
- * Helper class storing European Union languages information.
- *
- * @see https://github.com/openeuropa/oe_theme/blob/HEAD/modules/oe_theme_helper/src/EuropeanUnionLanguages.php
- */
-class EuropeanUnionLanguages {
-
-  /**
-   * List of European Union languages.
-   *
-   * Each entry includes:
-   *
-   * - The language name in English
-   * - The language name in its native form
-   * - The internal language ID, used on URLs, asset names, etc.
-   *
-   * @var array
-   */
-  protected static $languages = [
-    'bg' => ['Bulgarian', 'български', 'bg'],
-    'cs' => ['Czech', 'čeština', 'cs'],
-    'da' => ['Danish', 'dansk', 'da'],
-    'de' => ['German', 'Deutsch', 'de'],
-    'et' => ['Estonian', 'eesti', 'et'],
-    'el' => ['Greek', 'ελληνικά', 'el'],
-    'en' => ['English', 'English', 'en'],
-    'es' => ['Spanish', 'español', 'es'],
-    'fr' => ['French', 'français', 'fr'],
-    'ga' => ['Irish', 'Gaeilge', 'ga'],
-    'hr' => ['Croatian', 'hrvatski', 'hr'],
-    'it' => ['Italian', 'italiano', 'it'],
-    'lt' => ['Lithuanian', 'lietuvių', 'lt'],
-    'lv' => ['Latvian', 'latviešu', 'lv'],
-    'hu' => ['Hungarian', 'magyar', 'hu'],
-    'mt' => ['Maltese', 'Malti', 'mt'],
-    'nl' => ['Dutch', 'Nederlands', 'nl'],
-    'pl' => ['Polish', 'polski', 'pl'],
-    'pt-pt' => ['Portuguese', 'português', 'pt'],
-    'ro' => ['Romanian', 'română', 'ro'],
-    'sk' => ['Slovak', 'slovenčina', 'sk'],
-    'sl' => ['Slovenian', 'slovenščina', 'sl'],
-    'fi' => ['Finnish', 'suomi', 'fi'],
-    'sv' => ['Swedish', 'svenska', 'sv'],
-  ];
-
-  /**
-   * Returns a list of language data.
-   *
-   * This is the data that is expected to be returned by the overridden language
-   * manager as supplied by the OpenEuropa Multilingual module.
-   *
-   * @return array
-   *   An array with language codes as keys, and English and native language
-   *   names as values.
-   */
-  public static function getLanguageList(): array {
-    return self::$languages;
-  }
-
-  /**
-   * Assert whether the given language is a European Union one.
-   *
-   * @param string $language_code
-   *   The language code as defined by the W3C language tags document.
-   *
-   * @return bool
-   *   Whereas the given language is a European Union one.
-   */
-  public static function hasLanguage(string $language_code): bool {
-    return isset(self::$languages[$language_code]);
-  }
-
-  /**
-   * Get the language name in English given its W3C code.
-   *
-   * @param string $language_code
-   *   The language code as defined by the W3C language tags document.
-   *
-   * @return string
-   *   The language name in English if any, an empty string otherwise.
-   */
-  public static function getEnglishLanguageName(string $language_code): string {
-    return self::$languages[$language_code][0] ?? '';
-  }
-
-  /**
-   * Get the native language name given its W3C code.
-   *
-   * @param string $language_code
-   *   The language code as defined by the W3C language tags document.
-   *
-   * @return string
-   *   The native language name if any, an empty string otherwise.
-   */
-  public static function getNativeLanguageName(string $language_code): string {
-    return self::$languages[$language_code][1] ?? '';
-  }
-
-  /**
-   * Get the internal language code given its W3C code.
-   *
-   * Internal language codes may differ from the standard ones.
-   *
-   * @param string $language_code
-   *   The language code as defined by the W3C language tags document.
-   *
-   * @return string
-   *   The internal language code if any, an empty string otherwise.
-   */
-  public static function getInternalLanguageCode(string $language_code): string {
-    return self::$languages[$language_code][2] ?? '';
-  }
-
-}
diff --git a/modules/oe_whitelabel_helper/src/TwigExtension/TwigExtension.php b/modules/oe_whitelabel_helper/src/TwigExtension/TwigExtension.php
index 944b17e5fa625af94525c1631f9d1a6d2eb700e2..e667825177985d5331d24f9d741dfb4a5b50f1a8 100644
--- a/modules/oe_whitelabel_helper/src/TwigExtension/TwigExtension.php
+++ b/modules/oe_whitelabel_helper/src/TwigExtension/TwigExtension.php
@@ -6,7 +6,6 @@ namespace Drupal\oe_whitelabel_helper\TwigExtension;
 
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
-use Drupal\oe_whitelabel_helper\EuropeanUnionLanguages;
 use Drupal\Core\Url;
 use Twig\Extension\AbstractExtension;
 use Twig\TwigFilter;
@@ -37,7 +36,6 @@ class TwigExtension extends AbstractExtension {
   public function getFilters(): array {
     return [
       new TwigFilter('bcl_timeago', [$this, 'bclTimeAgo']),
-      new TwigFilter('to_internal_language_id', [$this, 'toInternalLanguageId']),
     ];
   }
 
@@ -184,21 +182,4 @@ class TwigExtension extends AbstractExtension {
     return $block_plugin->build();
   }
 
-  /**
-   * Get an internal language ID given its code.
-   *
-   * @param string $language_code
-   *   The language code as defined by the W3C language tags document.
-   *
-   * @return string
-   *   The internal language ID, or the given language code if none found.
-   */
-  public function toInternalLanguageId(string $language_code): string {
-    if (EuropeanUnionLanguages::hasLanguage($language_code)) {
-      return EuropeanUnionLanguages::getInternalLanguageCode($language_code);
-    }
-
-    return $language_code;
-  }
-
 }
diff --git a/modules/oe_whitelabel_multilingual/oe_whitelabel_multilingual.module b/modules/oe_whitelabel_multilingual/oe_whitelabel_multilingual.module
index 1dc9264e18018eb5f766cd521a7382da0ec50495..85fdf74f758967d9e56ded9a7b72b1b0b8848dc5 100755
--- a/modules/oe_whitelabel_multilingual/oe_whitelabel_multilingual.module
+++ b/modules/oe_whitelabel_multilingual/oe_whitelabel_multilingual.module
@@ -8,7 +8,7 @@
 declare(strict_types =  1);
 
 use Drupal\Component\Utility\Html;
-use Drupal\oe_whitelabel_helper\EuropeanUnionLanguages;
+use Drupal\oe_bootstrap_theme_helper\EuropeanUnionLanguages;
 
 /**
  * Implements hook_preprocess_links().
diff --git a/modules/oe_whitelabel_paragraphs/tests/src/Kernel/Paragraphs/DocumentParagraphTest.php b/modules/oe_whitelabel_paragraphs/tests/src/Kernel/Paragraphs/DocumentParagraphTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ca6a7926f94c630e43f13520356861af76e83f7
--- /dev/null
+++ b/modules/oe_whitelabel_paragraphs/tests/src/Kernel/Paragraphs/DocumentParagraphTest.php
@@ -0,0 +1,234 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\oe_whitelabel_paragraphs\Kernel\Paragraphs;
+
+use Drupal\Core\Site\Settings;
+use Drupal\file\Entity\File;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\media\Entity\Media;
+use Drupal\paragraphs\Entity\Paragraph;
+use Drupal\Tests\oe_bootstrap_theme\PatternAssertion\FilePatternAssert;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\user\Entity\User;
+use Symfony\Component\DomCrawler\Crawler;
+
+/**
+ * Tests the document paragraph.
+ */
+class DocumentParagraphTest extends ParagraphsTestBase {
+
+  use UserCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'content_translation',
+    'file_link_test',
+    'language',
+    'node',
+    'oe_paragraphs_document',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // The node dependency is wrongfully forced by oe_media_media_access().
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('media');
+    $this->installConfig([
+      'content_translation',
+      'language',
+      'media',
+      'oe_media',
+    ]);
+
+    $this->container->get('module_handler')->loadInclude('oe_paragraphs_media_field_storage', 'install');
+    oe_paragraphs_media_field_storage_install(FALSE);
+    $this->installConfig(['oe_paragraphs_document']);
+
+    ConfigurableLanguage::createFromLangcode('it')->save();
+    ConfigurableLanguage::createFromLangcode('es')->save();
+
+    // Enable translations for the document media bundle.
+    $this->container->get('content_translation.manager')->setEnabled('media', 'document', TRUE);
+    // Make fields translatable.
+    $field_ids = [
+      'media.document.oe_media_file_type',
+      'media.document.oe_media_remote_file',
+      'media.document.oe_media_file',
+    ];
+    foreach ($field_ids as $field_id) {
+      $field_config = $this->container->get('entity_type.manager')->getStorage('field_config')->load($field_id);
+      $field_config->set('translatable', TRUE)->save();
+    }
+    $this->container->get('router.builder')->rebuild();
+
+    // Simulate the presence of test remote files. This avoids real requests to
+    // external websites.
+    $settings = Settings::getAll();
+    $settings['file_link_test_middleware'] = [
+      'http://oe_whitelabel.drupal/spanish-document.txt' => [
+        'status' => 200,
+        'headers' => [
+          'Content-Type' => 'text/plain',
+          'Content-Length' => 45187,
+        ],
+      ],
+      'http://oe_whitelabel.drupal/spreadsheet.xls' => [
+        'status' => 200,
+        'headers' => [
+          'Content-Type' => 'application/vnd.ms-excel',
+          'Content-Length' => 78459784,
+        ],
+      ],
+    ];
+    new Settings($settings);
+
+    // Tests need to run with user 1 as access checks prevent entity reference
+    // rendering otherwise.
+    $this->setCurrentUser(User::load(1));
+  }
+
+  /**
+   * Tests the file paragraph rendering.
+   */
+  public function testRendering(): void {
+    $uri_en = $this->container->get('file_system')->copy(
+      $this->container->get('extension.list.module')->getPath('oe_media') . '/tests/fixtures/sample.pdf',
+      'public://test.pdf'
+    );
+    $pdf_en = File::create(['uri' => $uri_en]);
+    $pdf_en->save();
+
+    $local_media = Media::create([
+      'bundle' => 'document',
+      'name' => 'Local PDF file',
+      'oe_media_file_type' => 'local',
+      'oe_media_file' => [
+        'target_id' => $pdf_en->id(),
+      ],
+    ]);
+    $local_media->save();
+
+    $paragraph = Paragraph::create([
+      'type' => 'oe_document',
+      'field_oe_media' => [
+        'target_id' => $local_media->id(),
+      ],
+    ]);
+    $paragraph->save();
+
+    $html = $this->renderParagraph($paragraph);
+    $crawler = new Crawler($html);
+    $paragraph_wrapper = $crawler->filter('.paragraph');
+    $this->assertCount(1, $paragraph_wrapper);
+
+    $expected = [
+      'file' => [
+        'title' => 'Local PDF file',
+        'language' => 'English',
+        'url' => file_create_url($uri_en),
+        'meta' => '(2.96 KB - PDF)',
+        'icon' => 'file-pdf-fill',
+      ],
+      'translations' => NULL,
+      'link_label' => 'Download',
+    ];
+    $assert = new FilePatternAssert();
+    $assert->assertPattern($expected, $paragraph_wrapper->html());
+
+    // Add an Italian translation for the media.
+    $uri_it = $this->container->get('file_system')->copy(
+      $this->container->get('extension.list.module')->getPath('oe_media') . '/tests/fixtures/sample.pdf',
+      'public://test_it.pdf'
+    );
+    $pdf_it = File::create(['uri' => $uri_it]);
+    $pdf_it->save();
+    $local_media->addTranslation('it', [
+      'name' => 'Italian translation',
+      'oe_media_file_type' => 'local',
+      'oe_media_file' => [
+        'target_id' => $pdf_it->id(),
+      ],
+    ]);
+    $local_media->save();
+
+    $html = $this->renderParagraph($paragraph);
+    $crawler = new Crawler($html);
+    $paragraph_wrapper = $crawler->filter('.paragraph');
+    $this->assertCount(1, $paragraph_wrapper);
+    $expected['translations'] = [
+      [
+        'title' => 'Italian translation',
+        'language' => 'Italian',
+        'url' => file_create_url($uri_it),
+        'meta' => '(2.96 KB - PDF)',
+      ],
+    ];
+    $assert->assertPattern($expected, $paragraph_wrapper->html());
+
+    // Add a Spanish translation that points to a remote file.
+    $local_media->addTranslation('es', [
+      'name' => 'Spanish translation',
+      'oe_media_file_type' => 'remote',
+      'oe_media_remote_file' => 'http://oe_whitelabel.drupal/spanish-document.txt',
+    ]);
+    $local_media->save();
+
+    $html = $this->renderParagraph($paragraph);
+    $crawler = new Crawler($html);
+    $paragraph_wrapper = $crawler->filter('.paragraph');
+    $this->assertCount(1, $paragraph_wrapper);
+    $expected['translations'][] = [
+      'title' => 'Spanish translation',
+      'language' => 'Spanish',
+      'url' => 'http://oe_whitelabel.drupal/spanish-document.txt',
+      'meta' => '(44.13 KB - TXT)',
+    ];
+    $assert->assertPattern($expected, $paragraph_wrapper->html());
+
+    // Test a remote document as main file, to make sure that the
+    // DocumentMediaWrapper class is tested in all scenarios.
+    $remote_media = Media::create([
+      'bundle' => 'document',
+      'name' => 'Remote XLS file',
+      'oe_media_file_type' => 'remote',
+      'oe_media_remote_file' => 'http://oe_whitelabel.drupal/spreadsheet.xls',
+    ]);
+    $remote_media->save();
+
+    $paragraph = Paragraph::create([
+      'type' => 'oe_document',
+      'field_oe_media' => [
+        'target_id' => $remote_media->id(),
+      ],
+    ]);
+    $paragraph->save();
+
+    $html = $this->renderParagraph($paragraph);
+    $crawler = new Crawler($html);
+    $paragraph_wrapper = $crawler->filter('.paragraph');
+    $this->assertCount(1, $paragraph_wrapper);
+
+    $expected = [
+      'file' => [
+        'title' => 'Remote XLS file',
+        'language' => 'English',
+        'url' => 'http://oe_whitelabel.drupal/spreadsheet.xls',
+        'meta' => '(74.83 MB - XLS)',
+        'icon' => 'file-excel-fill',
+      ],
+      'translations' => NULL,
+      'link_label' => 'Download',
+    ];
+    $assert = new FilePatternAssert();
+    $assert->assertPattern($expected, $paragraph_wrapper->html());
+  }
+
+}
diff --git a/oe_whitelabel.theme b/oe_whitelabel.theme
index 5da6c190eebd347b07f207bc2265114b0e6df528..6be05efd19aac9b9da2c23fe12627f36130b6e1b 100644
--- a/oe_whitelabel.theme
+++ b/oe_whitelabel.theme
@@ -9,7 +9,8 @@ declare(strict_types = 1);
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
-use Drupal\oe_whitelabel_helper\EuropeanUnionLanguages;
+use Drupal\oe_bootstrap_theme_helper\EuropeanUnionLanguages;
+use Drupal\oe_whitelabel\DocumentMediaWrapper;
 
 // Include all files from the includes directory.
 $includes_path = __DIR__ . '/includes/*.inc';
@@ -137,3 +138,33 @@ function oe_whitelabel_preprocess_page(&$variables) {
   }
   $variables['site_logo_href'] = $site_logo_href;
 }
+
+/**
+ * Implements hook_preprocess_HOOK() for document media bundle.
+ */
+function oe_whitelabel_preprocess_media__document__default(&$variables) {
+  /** @var \Drupal\media\Entity\Media $media */
+  $media = $variables['media'];
+
+  $wrapper = new DocumentMediaWrapper($media);
+  if ($wrapper->isEmpty()) {
+    return;
+  }
+
+  $variables['file'] = $wrapper->toFileValueObject();
+
+  // Generate the file information for all available translations.
+  foreach ($media->getTranslationLanguages() as $langcode => $language) {
+    // We don't want to include the information of the current language again.
+    if ($media->language()->getId() === $langcode) {
+      continue;
+    }
+
+    $translation = $media->getTranslation($langcode);
+    $wrapper = new DocumentMediaWrapper($translation);
+    if ($wrapper->isEmpty()) {
+      continue;
+    }
+    $variables['translations'][] = $wrapper->toFileValueObject();
+  }
+}
diff --git a/runner.yml.dist b/runner.yml.dist
index 17e68fd13c793c6c8bfc4a1d7bff8b029f845a01..78ae4aaad5f680e9b2357fcc5c4e69f180121c7b 100644
--- a/runner.yml.dist
+++ b/runner.yml.dist
@@ -41,6 +41,7 @@ drupal:
         - "bower_components"
         - "vendor"
         - "${drupal.root}"
+      file_private_path: 'sites/default/files/private'
     databases:
       sparql_default:
         default:
diff --git a/src/DocumentMediaWrapper.php b/src/DocumentMediaWrapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..b486b3b8d5b40ec10d6535d4bd37e797623a732f
--- /dev/null
+++ b/src/DocumentMediaWrapper.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\oe_whitelabel;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\media\MediaInterface;
+use Drupal\oe_bootstrap_theme\ValueObject\FileValueObject;
+
+/**
+ * Wraps a media entity of bundle "document".
+ *
+ * @internal
+ */
+class DocumentMediaWrapper {
+
+  /**
+   * The media.
+   *
+   * @var \Drupal\media\MediaInterface
+   */
+  protected MediaInterface $media;
+
+  /**
+   * Construct a new wrapper object.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   The media to wrap.
+   */
+  public function __construct(MediaInterface $media) {
+    if ($media->bundle() !== 'document') {
+      throw new \InvalidArgumentException(sprintf('Invalid media of type "%s" passed, "document" expected.', $media->bundle()));
+    }
+
+    $this->media = $media;
+  }
+
+  /**
+   * Returns if the media is empty.
+   *
+   * A media is considered empty if the current active field, based on the type,
+   * is empty.
+   *
+   * @return bool
+   *   Whether the media is referencing a field or not.
+   */
+  public function isEmpty(): bool {
+    $field = $this->getActiveField();
+    return !$field || $field->isEmpty();
+  }
+
+  /**
+   * Returns the type of the media.
+   *
+   * @return string|null
+   *   The media type, usually "remote" or "local". NULL if no value set.
+   */
+  public function getType(): ?string {
+    return $this->media->get('oe_media_file_type')->value;
+  }
+
+  /**
+   * Creates a file value object from the current media values.
+   *
+   * @return \Drupal\oe_bootstrap_theme\ValueObject\FileValueObject|null
+   *   A file value object, or NULL if the media is empty.
+   */
+  public function toFileValueObject(): ?FileValueObject {
+    if ($this->isEmpty()) {
+      return NULL;
+    }
+
+    $field = $this->getActiveField();
+    $object = $this->getType() === 'remote'
+      ? FileValueObject::fromFileLink($field->first())
+      : FileValueObject::fromFileEntity($field->first()->entity);
+
+    return $object->setTitle($this->media->getName())
+      ->setLanguageCode($this->media->language()->getId());
+  }
+
+  /**
+   * Returns the field that is being used for the document, based on the type.
+   *
+   * @return \Drupal\Core\Field\FieldItemListInterface|null
+   *   The field item list, or NULL if an invalid type is specified.
+   */
+  protected function getActiveField(): ?FieldItemListInterface {
+    if (!$this->getType()) {
+      return NULL;
+    }
+
+    switch ($this->getType()) {
+      case 'remote':
+        return $this->media->get('oe_media_remote_file');
+
+      case 'local':
+        return $this->media->get('oe_media_file');
+
+      default:
+        return NULL;
+    }
+  }
+
+}
diff --git a/templates/media/media--document--default.html.twig b/templates/media/media--document--default.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..dcaaf033af0fc12effb79e1d307adc2823f436ba
--- /dev/null
+++ b/templates/media/media--document--default.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Theme override to display a media item.
+ *
+ * @see ./core/themes/stable/templates/content/media.html.twig
+ */
+#}
+{% if file %}
+  {{ pattern('file', {
+    file: file,
+    translations: translations,
+  }) }}
+{% endif %}