Multilingual Content-Strategy: Drupal 11 mit DeepL für 30+ Sprachen

Multilingual Content-Strategy: Drupal 11 mit DeepL für 30+ Sprachen

Die Verwaltung mehrsprachiger Websites mit 30+ Sprachen stellt besondere Herausforderungen an Content-Management, Translation-Workflows und Performance. In diesem Enterprise-Guide zeige ich, wie Sie mit Drupal 11 und der DeepL API eine skalierbare, kosteneffiziente Multilingual-Lösung aufbauen.

Warum Drupal 11 + DeepL?

Vorteile der Kombination

Drupal 11 Multilingual:

  • Native i18n Support seit Core
  • Content Translation Module (Core)
  • Configuration Translation
  • Interface Translation
  • Language Detection & Selection

DeepL API:

  • 31 Sprachen verfügbar (Stand 2025)
  • Formality API (Du/Sie, Tu/Vous)
  • Glossary Support für Fachterminologie
  • Context-aware Übersetzungen
  • EU-Server, DSGVO-konform

Kostenvorteil:

  • DeepL: ~€5-20 pro 1 Million Zeichen
  • Professionelle Übersetzer: €150-300 pro 1000 Wörter
  • Einsparung: 95-98% bei akzeptabler Qualität

Architektur-Übersicht

┌─────────────────────────────────────────┐
│   Content Creation (Source Language)   │
└──────────────────┬──────────────────────┘

         ┌─────────▼─────────┐
         │ Translation Queue │
         └─────────┬─────────┘

    ┌──────────────┼──────────────┐
    │              │              │
┌───▼───┐     ┌───▼───┐     ┌───▼───┐
│DeepL  │     │Human  │     │Hybrid │
│  API  │     │Review │     │Workflow│
└───┬───┘     └───┬───┘     └───┬───┘
    │              │              │
    └──────────────┼──────────────┘

         ┌─────────▼─────────┐
         │  Published Langs  │
         └───────────────────┘

Setup: Drupal 11 Multilingual

Schritt 1: Core Module aktivieren

# Language Module
drush en language -y

# Content Translation
drush en content_translation -y

# Configuration Translation
drush en config_translation -y

# Interface Translation
drush en locale -y

# Language Icons (optional)
composer require drupal/languageicons
drush en languageicons -y

Schritt 2: Sprachen hinzufügen

Via Drush (empfohlen für bulk):

# Einzelne Sprache
drush language:add de

# Multiple Sprachen
LANGUAGES=(de fr es it nl pl pt ru ja zh-hans)
for lang in "${LANGUAGES[@]}"; do
  drush language:add $lang
done

Via UI: /admin/config/regional/language/add

Schritt 3: Content Types konfigurieren

// In einem Custom Module: mymodule.install

/**
 * Enable translation for node types.
 */
function mymodule_update_9001() {
  $entity_types = ['node', 'taxonomy_term', 'block_content'];
  $bundles = [
    'node' => ['article', 'page', 'product'],
    'taxonomy_term' => ['tags', 'categories'],
    'block_content' => ['basic'],
  ];

  foreach ($entity_types as $entity_type) {
    foreach ($bundles[$entity_type] as $bundle) {
      \Drupal::service('content_translation.manager')
        ->setEnabled($entity_type, $bundle, TRUE);
    }
  }
}

Via UI: /admin/config/regional/content-language

DeepL Glossary Integration

Glossare ermöglichen konsistente Übersetzung von Fachbegriffen.

Glossar erstellen

<?php

namespace Drupal\mymodule\Service;

use DeepL\Translator;
use DeepL\GlossaryEntries;

/**
 * DeepL Glossary Manager.
 */
class DeepLGlossaryManager {

  /**
   * The DeepL Translator.
   *
   * @var \DeepL\Translator
   */
  protected $translator;

  /**
   * Constructor.
   */
  public function __construct() {
    $api_key = \Drupal::config('deepl_translator.settings')->get('api_key');
    $this->translator = new Translator($api_key);
  }

  /**
   * Creates a glossary.
   *
   * @param string $name
   *   Glossary name.
   * @param string $source_lang
   *   Source language.
   * @param string $target_lang
   *   Target language.
   * @param array $entries
   *   Key-value pairs of terms.
   *
   * @return string
   *   Glossary ID.
   */
  public function createGlossary(
    string $name,
    string $source_lang,
    string $target_lang,
    array $entries
  ): string {

    $glossary_entries = GlossaryEntries::fromEntries($entries);

    $glossary = $this->translator->createGlossary(
      $name,
      $source_lang,
      $target_lang,
      $glossary_entries
    );

    // Speichern für später
    \Drupal::state()->set("deepl_glossary_{$source_lang}_{$target_lang}", [
      'id' => $glossary->glossaryId,
      'name' => $name,
      'source_lang' => $source_lang,
      'target_lang' => $target_lang,
      'entry_count' => $glossary->entryCount,
    ]);

    return $glossary->glossaryId;
  }

  /**
   * Gets glossary for language pair.
   */
  public function getGlossaryId(string $source_lang, string $target_lang): ?string {
    $glossary = \Drupal::state()->get("deepl_glossary_{$source_lang}_{$target_lang}");
    return $glossary['id'] ?? null;
  }

  /**
   * Lists all glossaries.
   */
  public function listGlossaries(): array {
    $glossaries = $this->translator->listGlossaries();

    $result = [];
    foreach ($glossaries as $glossary) {
      $result[] = [
        'id' => $glossary->glossaryId,
        'name' => $glossary->name,
        'source' => $glossary->sourceLang,
        'target' => $glossary->targetLang,
        'entries' => $glossary->entryCount,
        'created' => $glossary->creationTime,
      ];
    }

    return $result;
  }

  /**
   * Deletes a glossary.
   */
  public function deleteGlossary(string $glossary_id): void {
    $this->translator->deleteGlossary($glossary_id);
  }

}

Glossar verwenden

// Beispiel: Technisches Glossar
$technical_terms = [
  'API' => 'API',
  'Backend' => 'Backend',
  'Frontend' => 'Frontend',
  'Cache' => 'Cache',
  'Database' => 'Datenbank',
  'Server' => 'Server',
  'User' => 'Benutzer',
  'Admin' => 'Administrator',
  'Login' => 'Anmeldung',
  'Logout' => 'Abmeldung',
];

$glossary_manager = \Drupal::service('mymodule.deepl_glossary');
$glossary_id = $glossary_manager->createGlossary(
  'Technical Terms EN-DE',
  'EN',
  'DE',
  $technical_terms
);

// Übersetzen mit Glossar
$translator = \Drupal::service('deepl_translator.client');
$translation = $translator->translateText(
  'Please login to access the admin backend.',
  'DE',
  'EN',
  ['glossary_id' => $glossary_id]
);

// Ergebnis: "Bitte melden Sie sich an, um auf das Administrator-Backend zuzugreifen."

Translation Workflow

1. Automated Translation Workflow

<?php

namespace Drupal\mymodule\Service;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Queue\QueueFactory;

/**
 * Translation Workflow Manager.
 */
class TranslationWorkflow {

  /**
   * Queue a node for translation.
   */
  public function queueTranslation(
    ContentEntityInterface $entity,
    array $target_languages,
    string $workflow = 'auto'
  ): void {

    $queue = \Drupal::queue('deepl_translation_queue');

    foreach ($target_languages as $lang) {
      $item = [
        'entity_type' => $entity->getEntityTypeId(),
        'entity_id' => $entity->id(),
        'target_language' => $lang,
        'workflow' => $workflow,
        'timestamp' => time(),
      ];

      $queue->createItem($item);
    }

    \Drupal::logger('translation_workflow')->info('Queued @entity for translation to @count languages', [
      '@entity' => $entity->label(),
      '@count' => count($target_languages),
    ]);
  }

  /**
   * Process translation queue.
   */
  public function processQueue(): void {
    $queue = \Drupal::queue('deepl_translation_queue');
    $queue_worker = \Drupal::service('plugin.manager.queue_worker')
      ->createInstance('deepl_translation_queue');

    while ($item = $queue->claimItem()) {
      try {
        $queue_worker->processItem($item->data);
        $queue->deleteItem($item);
      }
      catch (\Exception $e) {
        \Drupal::logger('translation_workflow')->error('Failed to process queue item: @error', [
          '@error' => $e->getMessage(),
        ]);

        // Release item for retry
        $queue->releaseItem($item);
      }
    }
  }

}

2. Hybrid Workflow (Auto + Human Review)

/**
 * Translation with review workflow.
 */
class HybridTranslationWorkflow {

  /**
   * Translate with pending review state.
   */
  public function translateWithReview(
    ContentEntityInterface $entity,
    string $target_language
  ): void {

    // 1. Auto-translate with DeepL
    $translator = \Drupal::service('deepl_translator.translator');
    $translation = $translator->translateEntity($entity, $target_language);

    // 2. Set moderation state to "needs_review"
    if ($translation->hasField('moderation_state')) {
      $translation->set('moderation_state', 'needs_review');
    }

    // 3. Add metadata for reviewer
    $translation->set('field_translation_info', [
      'value' => json_encode([
        'auto_translated' => true,
        'service' => 'DeepL',
        'translated_at' => date('c'),
        'source_language' => $entity->language()->getId(),
        'requires_review' => true,
      ]),
    ]);

    // 4. Notify reviewer
    $this->notifyReviewer($translation, $target_language);

    $translation->save();
  }

  /**
   * Notify translator for review.
   */
  protected function notifyReviewer($entity, $lang): void {
    $reviewer = $this->getReviewerForLanguage($lang);

    if ($reviewer) {
      $params = [
        'entity' => $entity,
        'language' => $lang,
        'edit_url' => $entity->toUrl('edit-form')->setAbsolute()->toString(),
      ];

      \Drupal::service('plugin.manager.mail')->mail(
        'mymodule',
        'translation_review',
        $reviewer->getEmail(),
        $lang,
        $params
      );
    }
  }

}

Content Strategy

Übersetzungs-Prioritäten

/**
 * Content Translation Priority Manager.
 */
class TranslationPriority {

  /**
   * Priority levels.
   */
  const PRIORITY_CRITICAL = 1;  // Homepage, Landing Pages
  const PRIORITY_HIGH = 2;      // Products, Services
  const PRIORITY_MEDIUM = 3;    // Blog Posts, News
  const PRIORITY_LOW = 4;       // Archive, Documentation

  /**
   * Determine translation priority.
   */
  public function getPriority(ContentEntityInterface $entity): int {
    // Critical: Promoted content
    if ($entity->hasField('promote') && $entity->get('promote')->value) {
      return self::PRIORITY_CRITICAL;
    }

    // High: Product content types
    if ($entity->bundle() === 'product') {
      return self::PRIORITY_HIGH;
    }

    // Medium: Recent content
    if ($entity->hasField('created')) {
      $created = $entity->get('created')->value;
      if ($created > (time() - 2592000)) { // 30 days
        return self::PRIORITY_MEDIUM;
      }
    }

    return self::PRIORITY_LOW;
  }

  /**
   * Get languages based on priority.
   */
  public function getTargetLanguages(int $priority): array {
    $all_languages = \Drupal::languageManager()->getLanguages();

    switch ($priority) {
      case self::PRIORITY_CRITICAL:
        // All languages
        return array_keys($all_languages);

      case self::PRIORITY_HIGH:
        // Major European languages
        return ['de', 'fr', 'es', 'it', 'nl', 'pl'];

      case self::PRIORITY_MEDIUM:
        // Top 3 languages
        return ['de', 'fr', 'es'];

      case self::PRIORITY_LOW:
        // On-demand only
        return [];
    }

    return [];
  }

}

Formality Strategy

/**
 * Formality Manager for different content types.
 */
class FormalityStrategy {

  /**
   * Determine formality level.
   */
  public function getFormality(ContentEntityInterface $entity): string {
    // B2B Content: Formal
    if ($entity->hasField('field_audience') &&
        $entity->get('field_audience')->value === 'b2b') {
      return 'more'; // Sie/Vous/Usted
    }

    // B2C Content: Informal
    if ($entity->bundle() === 'blog_post') {
      return 'less'; // Du/Tu/Tú
    }

    // Legal/Compliance: Formal
    if ($entity->bundle() === 'legal_document') {
      return 'more';
    }

    // Default: Context-aware
    return 'default';
  }

}

Performance Optimization

1. Batch Processing

/**
 * Batch translation for large content sets.
 */
class BatchTranslation {

  /**
   * Create batch for mass translation.
   */
  public static function createBatch(array $entity_ids, array $target_languages): array {
    $operations = [];

    // Split into chunks of 10
    $chunks = array_chunk($entity_ids, 10);

    foreach ($chunks as $chunk) {
      foreach ($target_languages as $lang) {
        $operations[] = [
          [self::class, 'processBatch'],
          [$chunk, $lang],
        ];
      }
    }

    return [
      'title' => t('Translating content...'),
      'operations' => $operations,
      'finished' => [self::class, 'finishBatch'],
      'progressive' => true,
    ];
  }

  /**
   * Process batch operation.
   */
  public static function processBatch($entity_ids, $target_lang, &$context) {
    $translator = \Drupal::service('deepl_translator.translator');
    $storage = \Drupal::entityTypeManager()->getStorage('node');

    foreach ($entity_ids as $id) {
      $entity = $storage->load($id);

      if ($entity && !$entity->hasTranslation($target_lang)) {
        try {
          $translator->translateEntity($entity, $target_lang);
          $context['results']['success'][] = $id;
        }
        catch (\Exception $e) {
          $context['results']['failed'][] = $id;
        }
      }
    }

    $context['message'] = t('Translated @count entities', [
      '@count' => count($context['results']['success']),
    ]);
  }

}

2. Caching Strategy

/**
 * Translation cache manager.
 */
class TranslationCache {

  /**
   * Cache translation result.
   */
  public function cacheTranslation(
    string $text,
    string $source,
    string $target,
    string $result
  ): void {

    $cache_key = $this->generateCacheKey($text, $source, $target);

    \Drupal::cache('translation')->set(
      $cache_key,
      $result,
      time() + 2592000, // 30 days
      ['translation', "translation:$source:$target"]
    );
  }

  /**
   * Get cached translation.
   */
  public function getCachedTranslation(
    string $text,
    string $source,
    string $target
  ): ?string {

    $cache_key = $this->generateCacheKey($text, $source, $target);
    $cached = \Drupal::cache('translation')->get($cache_key);

    return $cached ? $cached->data : null;
  }

  /**
   * Generate cache key.
   */
  protected function generateCacheKey(
    string $text,
    string $source,
    string $target
  ): string {
    return 'translation:' . md5($text . $source . $target);
  }

}

SEO für Multilingual Sites

1. Hreflang Tags

<?php

/**
 * Implements hook_page_attachments().
 */
function mymodule_page_attachments(array &$attachments) {
  $route_match = \Drupal::routeMatch();

  if ($route_match->getRouteName() === 'entity.node.canonical') {
    $node = $route_match->getParameter('node');

    if ($node instanceof \Drupal\node\NodeInterface) {
      $languages = \Drupal::languageManager()->getLanguages();

      foreach ($languages as $language) {
        $lang_code = $language->getId();

        if ($node->hasTranslation($lang_code)) {
          $translation = $node->getTranslation($lang_code);
          $url = $translation->toUrl()->setAbsolute()->toString();

          $attachments['#attached']['html_head'][] = [
            [
              '#tag' => 'link',
              '#attributes' => [
                'rel' => 'alternate',
                'hreflang' => $lang_code,
                'href' => $url,
              ],
            ],
            'hreflang_' . $lang_code,
          ];
        }
      }
    }
  }
}

2. Structured Data per Language

/**
 * JSON-LD Structured Data for each language.
 */
function mymodule_preprocess_node(&$variables) {
  $node = $variables['node'];
  $current_lang = \Drupal::languageManager()->getCurrentLanguage()->getId();

  $structured_data = [
    '@context' => 'https://schema.org',
    '@type' => 'Article',
    'headline' => $node->getTranslation($current_lang)->getTitle(),
    'inLanguage' => $current_lang,
    'url' => $node->toUrl()->setAbsolute()->toString(),
  ];

  $variables['#attached']['html_head'][] = [
    [
      '#tag' => 'script',
      '#attributes' => ['type' => 'application/ld+json'],
      '#value' => json_encode($structured_data),
    ],
    'structured_data_article',
  ];
}

Cost Optimization

Übersetzungs-Budget Manager

/**
 * Translation Budget Manager.
 */
class BudgetManager {

  /**
   * Calculate translation cost.
   */
  public function calculateCost(
    ContentEntityInterface $entity,
    array $target_languages
  ): array {

    $total_chars = 0;
    $fields = $this->getTranslatableFields($entity);

    foreach ($fields as $field_name) {
      $value = $entity->get($field_name)->value;
      $total_chars += mb_strlen(strip_tags($value));
    }

    $chars_per_lang = $total_chars * count($target_languages);

    // DeepL Pricing: ~€0.00002 per character
    $cost_eur = $chars_per_lang * 0.00002;

    return [
      'characters' => $total_chars,
      'total_characters' => $chars_per_lang,
      'languages' => count($target_languages),
      'cost_eur' => round($cost_eur, 2),
      'cost_per_language' => round($cost_eur / count($target_languages), 2),
    ];
  }

  /**
   * Check if within budget.
   */
  public function isWithinBudget(float $cost): bool {
    $monthly_budget = \Drupal::config('mymodule.settings')->get('monthly_budget');
    $current_spend = \Drupal::state()->get('translation_spend_' . date('Y-m'), 0);

    return ($current_spend + $cost) <= $monthly_budget;
  }

}

Quality Assurance

Translation Quality Checker

/**
 * Translation Quality Checker.
 */
class QualityChecker {

  /**
   * Check translation quality.
   */
  public function checkQuality(
    string $source,
    string $translation,
    string $source_lang,
    string $target_lang
  ): array {

    $issues = [];

    // 1. Length check (translation shouldn't be 3x longer/shorter)
    $ratio = mb_strlen($translation) / mb_strlen($source);
    if ($ratio < 0.3 || $ratio > 3) {
      $issues[] = 'Length ratio unusual: ' . round($ratio, 2);
    }

    // 2. HTML tag consistency
    if ($this->hasHtmlTags($source)) {
      if (!$this->tagsMatch($source, $translation)) {
        $issues[] = 'HTML tags mismatch';
      }
    }

    // 3. Placeholder consistency
    $source_placeholders = $this->extractPlaceholders($source);
    $trans_placeholders = $this->extractPlaceholders($translation);

    if ($source_placeholders !== $trans_placeholders) {
      $issues[] = 'Placeholder mismatch';
    }

    // 4. Character encoding issues
    if (mb_detect_encoding($translation, 'UTF-8', true) === false) {
      $issues[] = 'Encoding issue detected';
    }

    return [
      'has_issues' => !empty($issues),
      'issues' => $issues,
      'quality_score' => $this->calculateScore($issues),
    ];
  }

  /**
   * Calculate quality score (0-100).
   */
  protected function calculateScore(array $issues): int {
    return max(0, 100 - (count($issues) * 20));
  }

}

Monitoring & Analytics

Translation Analytics Dashboard

/**
 * Translation Analytics Service.
 */
class TranslationAnalytics {

  /**
   * Get translation statistics.
   */
  public function getStatistics(string $period = 'month'): array {
    $start = strtotime("-1 $period");

    $query = \Drupal::database()->select('translation_log', 't');
    $query->fields('t', ['language', 'characters', 'cost']);
    $query->condition('timestamp', $start, '>=');
    $results = $query->execute()->fetchAll();

    $stats = [];
    foreach ($results as $row) {
      $lang = $row->language;

      if (!isset($stats[$lang])) {
        $stats[$lang] = [
          'translations' => 0,
          'characters' => 0,
          'cost' => 0,
        ];
      }

      $stats[$lang]['translations']++;
      $stats[$lang]['characters'] += $row->characters;
      $stats[$lang]['cost'] += $row->cost;
    }

    return $stats;
  }

  /**
   * Get most translated content types.
   */
  public function getTopContentTypes(): array {
    $query = \Drupal::database()->select('translation_log', 't');
    $query->fields('t', ['entity_type', 'bundle']);
    $query->addExpression('COUNT(*)', 'count');
    $query->groupBy('entity_type');
    $query->groupBy('bundle');
    $query->orderBy('count', 'DESC');
    $query->range(0, 10);

    return $query->execute()->fetchAll();
  }

}

Fazit

Die Kombination von Drupal 11 und DeepL ermöglicht Enterprise-Multilingual-Websites mit 30+ Sprachen bei überschaubarem Budget und Wartungsaufwand. Durch intelligente Workflows, Glossare und Quality-Assurance-Prozesse erreichen Sie professionelle Übersetzungsqualität bei Kostenersparnis von 95%+ gegenüber manueller Übersetzung.

Key Takeaways:

  • Glossare für konsistente Fachterminologie
  • Hybrid-Workflow (Auto + Human Review) für kritischen Content
  • Content-Priorisierung nach Business-Value
  • Performance-Optimierung durch Caching und Batch-Processing
  • Kontinuierliches Monitoring und Quality-Assurance

Planen Sie eine mehrsprachige Drupal 11 Website? Mit über 20 Jahren Erfahrung in komplexen Drupal-Projekten unterstütze ich Sie bei Architektur, Implementation und Optimierung. Kontakt: mail@stevenschulz.net oder 04037420859

← Zurück zum Blog