Multilingual Support
The AI Context module supports content translation for ai_context_item
entities. When AI consumers select context items, they receive content in
the detected content language when a translation exists.
How it works
Translation detection
Translation support is primarily handled in these services:
- AiContextLanguageService -- detects the content language and
applies entity translations via
translateItem()/translateItems() - AiContextSelector -- uses the language service before filtering and scoring
- AiContextRenderer -- renders the already-translated entities
AiContextSelector, the Language scope plugin, and function call plugins use
the shared AiContextLanguageService. This keeps translation selection and
Language scope hard filtering on the same content language.
AiContextLanguageService reads its path from
AiContextRequestInfoResolver::getPath(), the same effective-page-path
resolver Site Sections and Entity Types use.
getPath() uses the first source that returns a path, in this order:
- Caller-supplied path -- a path set explicitly via
setPath(), such asBuildSystemPromptEventtokens on an API-proxy request - Request body path -- a
pathvalue in POST data or a JSON request body - Referer header -- a same-site HTTP Referer, checked only on a request
Drupal recognizes as asynchronous (an
X-Requested-With: XMLHttpRequestheader or a JSON content-type). A Referer on a plain page load, or on a request without that async signal, is skipped - Request path -- the current request's own path (
getPathInfo())
Language detection then looks for a configured URL prefix on that one path.
A path without a prefix does not cause detection to try the next getPath()
source. For example, a caller-supplied /node/1 wins over a Referer of
/es/node/1, and detection falls through to Drupal language negotiation
instead of reading Spanish from the Referer.
If that chosen path has no language prefix, detection falls back to Drupal
language negotiation (getCurrentLanguage(TYPE_CONTENT)).
When there is no caller-supplied or body path, the Referer is checked before
the request's own path. On an asynchronous call from a language-prefixed page
(for example /es/...) to an unprefixed API-proxy endpoint, the Referer's
language wins even though the request path itself carries no prefix.
Language scope requires more than one language
Language scope is available only when the site has more than one language. With only one language (Language module off, or any single installed language):
- Languages does not appear on the context item form
- Languages never appears on the consumer subscription form (Languages does not support subscriptions)
- stored
languagevalues do not filter or score items - saving an item or consumer does not delete stored language values
Add a second language to enable Language scope on context items. Existing stored item values start applying without a re-save. Stored consumer language subscriptions stay inert. Removing the last extra language makes Language unavailable again; stored values remain.
Disabled scopes work the same way: they are ignored at runtime and hidden from forms, but form save does not strip their stored item values or consumer subscriptions.
Saving an item as Global, or as a subcontext that inherits parent scope, still clears stored language values. That is the existing Global / inherit rule, not the single-language hide-and-keep behavior.
Language scope and entity translation
Language scope and entity translation answer different questions:
- Language scope determines whether a Context Item is eligible for the request context. It is a hard filter and a situational matcher, not a subscription score.
- Entity translation determines which version of a selected Context Item is rendered.
Leaving Language scope blank makes an item language-neutral for matching. It does not make the item language-neutral content: Drupal still stores an original entity language, and the selector uses that original when a requested translation does not exist.
Language scope hard filtering uses the same content-language detection order as translation selection, so AJAX requests from a language-prefixed page filter and render with that language.
See Language scope for selection examples and Context Selection for where translation occurs in the pipeline.
Why not just getCurrentLanguage()?
Drupal's language negotiation relies on the request URL. AJAX-based tools
can POST to an unprefixed path even when the page the user is viewing is
language-prefixed. In that case, getCurrentLanguage() can return the
default language unless the Referer is also checked.
Fallback behavior
When a translation is not available for the detected language, the system falls back to the default language version of the content. This ensures that context remains available even when translations are incomplete.
Compatibility
The behavior works in all of these cases:
- content translation is enabled and translations exist
- content translation is enabled but translations do not exist
- content translation is not enabled
- the language module is not enabled
Setting up translations
Enabling translation
- Enable the Language and Content Translation modules
- Add additional languages at
/admin/config/regional/language - Enable translation for AI Context Items at
/admin/config/regional/content-language - Check "AI Context Item" and select which fields should be translatable
- Save the configuration
Adding translations
- Go to
/admin/config/ai/context/items - Click Edit on a context item
- Click the Translate tab
- Click Add for the language you want to translate to
- Enter the translated content
- Save the translation
Usage example
/** @var \Drupal\ai_context\Service\AiContextSelectionFactoryInterface $factory */
$factory = \Drupal::service('ai_context.selection_factory');
$result = $factory->getResult(
scopes: ['use_case' => ['working_with_text']],
);
$context_text = $result->getRenderedText();
Selection detects the content language from the request path or Referer; callers do not need to pass a Language scope solely to select a translation. If a translation is missing, the default-language entity is used instead.
Technical details
Safe translation checks
The implementation uses safe translation checks that work even when translation is not enabled:
if ($item->hasTranslation($detectedLanguage)) {
$item = $item->getTranslation($detectedLanguage);
}
Performance
- translation loading happens in the same entity-loading pass
- no extra translation work happens in the renderer
- language detection happens once per request
- selection results vary by the
ai_context.detected_languagecache context, which includes Referer-based detection for unprefixed AJAX requests
Migration notes
No migration is required. Translation support is automatically available once you:
- Update the module code
- Clear caches (
drush cr) - Enable content translation for
ai_context_itementities (optional) - Add translations to your context items (optional)
Troubleshooting
Context not translating
- Verify content translation is enabled for
ai_context_itemat/admin/config/regional/content-language - Check that the context item has a translation in the target language
- Verify the language is properly configured in Drupal at
/admin/config/regional/language - Clear all caches (
drush cr)
Wrong language returned
- Enable debug logging and check the
ai_contextlog messages for the detected language - Verify the AJAX caller is sending a Referer header (browsers do this by
default) and a signal that marks the request as asynchronous --
X-Requested-With: XMLHttpRequestor a JSON content-type. A Referer without one of these is ignored; a barefetch()call often needs the header added explicitly. - Verify that
language.negotiationconfig has the correcturl.prefixesmapping - On an async request with both a same-site Referer and a request path that
disagree on language, the Referer wins only when there is no
caller-supplied or body path;
getPath()checks Referer before the request path - If the chosen path has no language prefix, the system falls back to
getCurrentLanguage(TYPE_CONTENT). It does not try latergetPath()sources to find a prefix.
Translation not saving
- Verify the user has permission to translate content
- Check that all required fields are filled in the translation
- Verify the translation is marked as published
- Clear all caches after saving