Skip to content

Anti-Patterns & Common Mistakes

When to Use

When reviewing code or debugging multilingual issues — learn what NOT to do and why.

Anti-Pattern: Translating User Input

What NOT to do:

// NEVER DO THIS — XSS vulnerability
$user_input = $_POST['message'];
$message = $this->t($user_input);

// NEVER DO THIS — user content, not UI
$node_title = $node->getTitle();
$translated = $this->t($node_title);

Why it's wrong: - t() is for interface text, not user content - User input through t() is XSS attack vector - Translation database gets polluted with user content - User content should use entity translation, not interface translation

What to do instead:

// For user input — sanitize, don't translate
$user_input = $_POST['message'];
$safe_output = Html::escape($user_input);

// For entity content — use entity translation
$node = Node::load(1);
$spanish = $node->getTranslation('es');
$title = $spanish->getTitle(); // Already translated via entity API

Anti-Pattern: Concatenating Translated Strings

What NOT to do:

// WRONG — languages have different word orders
$message = $this->t('Hello') . ' ' . $user->getDisplayName() . ', ' . $this->t('welcome to our site!');

// WRONG — can't reorder for different languages
$label = $this->t('Posted by') . ' ' . $author . ' ' . $this->t('on') . ' ' . $date;

Why it's wrong: - Word order varies by language - Some languages put verb at end - Translators can't reorder concatenated parts - Results in broken grammar in other languages

What to do instead:

// RIGHT — single translatable string with placeholders
$message = $this->t('Hello @name, welcome to our site!', [
  '@name' => $user->getDisplayName(),
]);

$label = $this->t('Posted by @author on @date', [
  '@author' => $author,
  '@date' => $date,
]);

Anti-Pattern: Hardcoding Language Codes

What NOT to do:

// WRONG — assumes Spanish always exists and uses 'es'
if ($node->hasTranslation('es')) {
  $spanish = $node->getTranslation('es');
}

// WRONG — hardcodes default language
$config->set('default_langcode', 'en');

// WRONG — business logic depends on language code
if ($current_language == 'en') {
  // Special English-only logic
}

Why it's wrong: - Site might use different language codes (es-MX vs es, en-GB vs en) - Breaks if languages added/removed - Not portable across sites - Violates separation of concerns

What to do instead:

// RIGHT — use configuration
$default_language = \Drupal::languageManager()->getDefaultLanguage()->getId();
$available_languages = \Drupal::languageManager()->getLanguages();

// RIGHT — iterate all languages instead of hardcoding
foreach ($node->getTranslationLanguages() as $langcode => $language) {
  $translation = $node->getTranslation($langcode);
  // Process
}

// RIGHT — business logic independent of language
// Use content/config to control behavior, not language code

Anti-Pattern: Embedding Unsanitized Content in Translated Strings

What NOT to do:

// WRONG — user input directly in markup
$user_input = $_POST['name'];
$message = new FormattableMarkup('Hello @name', ['@name' => $user_input]);
// This is fine for FormattableMarkup, but don't confuse with t()

// WRONG — concatenating markup into t()
$message = $this->t('Hello ' . $user_input);
// XSS vulnerability — user input not sanitized

Why it's wrong: - Direct string concatenation in t() bypasses sanitization - User input must always go through placeholders - In Drupal 8+, only @, %, : placeholders are supported in t()

What to do instead:

// RIGHT — @placeholder auto-sanitizes in t()
$user_input = $_POST['name'];
$message = $this->t('Hello @name', ['@name' => $user_input]);

// RIGHT — for pre-sanitized markup, use FormattableMarkup (NOT t())
use Drupal\Component\Render\FormattableMarkup;
$markup = new FormattableMarkup('<strong>@label</strong>', ['@label' => $label]);

Anti-Pattern: Translating Non-Translatable Fields

What NOT to do:

// Create entity with non-translatable field
$node = Node::create(['type' => 'article']);
$node->set('field_date', '2026-02-16'); // Non-translatable
$node->save();

// Add translation and try to change non-translatable field
$spanish = $node->addTranslation('es');
$spanish->set('field_date', '2026-02-17'); // Changes ALL translations
$node->save();

// English node now has 2026-02-17 too — unexpected!

Why it's wrong: - Non-translatable fields share value across translations - Changing in one translation affects all - Users expect field to be independent but it's not

What to do instead:

// Only set translatable fields per translation
$spanish = $node->addTranslation('es');
$spanish->set('title', 'Título en español'); // Translatable
$spanish->set('body', 'Cuerpo en español');  // Translatable
// Don't set field_date — it's shared
$node->save();

// Or make field translatable if it needs different values
$field = FieldConfig::load('node.article.field_date');
$field->setTranslatable(TRUE)->save();

Anti-Pattern: Missing Cache Contexts

What NOT to do:

// WRONG — language-dependent render array without language cache context
function mymodule_block_view() {
  return [
    '#markup' => t('Welcome!'),
    '#cache' => [
      'max-age' => 3600,
      // Missing: 'contexts' => ['languages:language_interface']
    ],
  ];
}
// Cached in English, shown to Spanish users

Why it's wrong: - Cache doesn't vary by language - First visitor's language cached for everyone - Other language users see wrong language

What to do instead:

// RIGHT — include language cache context
function mymodule_block_view() {
  return [
    '#markup' => t('Welcome!'),
    '#cache' => [
      'max-age' => 3600,
      'contexts' => ['languages:language_interface'],
    ],
  ];
}

Anti-Pattern: Ignoring Translation Metadata

What NOT to do:

// Create translation without metadata
$node = Node::load(1);
$spanish = $node->addTranslation('es', ['title' => 'Título']);
$node->save();
// Missing: source language, translation author, timestamp

Why it's wrong: - Translation overview UI shows incomplete info - Can't track who translated what - Can't mark translations outdated - Breaks TMGMT integration

What to do instead:

// Set translation metadata
$node = Node::load(1);
$spanish = $node->addTranslation('es', ['title' => 'Título']);
$spanish->content_translation_source = 'en';
$spanish->content_translation_outdated = FALSE;
$spanish->content_translation_uid = \Drupal::currentUser()->id();
$spanish->content_translation_created = time();
$node->save();

See Also