Appearance
Are you an LLM? You can read better optimized documentation at /advanced_topics/request_culture.md for this page in Markdown format
Custom request culture providers
This document provides a detailed technical overview of the application's advanced localization strategy, focusing on the implementation of custom IRequestCultureProviders to manage and persist the user's selected locale. This system is designed to integrate seamlessly with Contentful's localization features while maintaining a separate set of localizations for the application's UI chrome.
For a higher-level overview of localization, please see the Localization feature documentation.
Localization deep dive
The application employs a dual-strategy for internationalization (i18n):
- Content Localization: All dynamic content (courses, lessons, etc.) is fetched from Contentful. The locale for this content is determined by the
localeparameter passed to the Contentful Delivery API. This allows content authors to manage translations directly within the CMS. - UI Localization: Static UI text (labels, button text, metadata, etc.) is managed within the application itself. These translations are stored in JSON files located at
/wwwroot/locales/(e.g.,en-US.json,de-DE.json). A customJsonViewLocalizeris used to load and serve these strings based on the current thread'sCultureInfo.
The core challenge is to synchronize these two systems. The application must determine the correct culture for both the Contentful API requests and the UI rendering, handle cases where a locale exists in Contentful but not in the local UI files, and persist the user's choice across requests. This is achieved using a chain of custom request culture providers.
CustomRequestCultureProvider
To implement the complex logic required for locale selection, the application bypasses the standard ASP.NET Core culture providers in favor of its own CustomRequestCultureProvider implementations. These are configured within the Configure method in Startup.cs.
See Service Configuration for more details on application startup.
Implementation in Startup.cs
The request localization middleware is configured with a specific list of providers. The key is that the application uses its own lambda-based CustomRequestCultureProvider instances to gain full control over how the culture is determined.
csharp
// File: TheExampleApp/Startup.cs
app.UseRequestLocalization(new RequestLocalizationOptions
{
RequestCultureProviders = new List<IRequestCultureProvider>
{
// 1. Provider to handle incoming locale changes from query string
new CustomRequestCultureProvider(async (s) => {
// ... logic to detect locale from query string, validate, and set session ...
}),
// 2. Standard provider (acts as a redundant fallback)
new QueryStringRequestCultureProvider()
{
QueryStringKey = LOCALE_KEY
},
// 3. Provider to read the established locale from the session
new CustomRequestCultureProvider(async (s) => {
// ... logic to read locale and fallback locale from session ...
})
},
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = SupportedCultures,
SupportedUICultures = SupportedCultures
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Locale detection from query string
The first provider in the chain is responsible for detecting an explicit locale change from the user, typically via a URL like /?locale=de-DE.
Its responsibilities are:
- Check if the
localequery string parameter exists. - Validate that the requested locale is configured in the Contentful space. This prevents errors from requesting content in a language the CMS doesn't support.
- If the locale is valid in Contentful but not supported by the application's static UI files, it initiates the fallback process (see Fallback chain).
- If the locale is valid, it stores the choice in the user's session for persistence.
csharp
// File: TheExampleApp/Startup.cs
// Inside the first CustomRequestCultureProvider
if (s.Request.Query.ContainsKey(LOCALE_KEY))
{
// Remove any previous fallback.
s.Session.Remove(FALLBACK_LOCALE_KEY);
var locale = s.Request.Query[LOCALE_KEY];
var client = s.RequestServices?.GetService<IContentfulClient>();
var contentfulLocales = new List<Locale>();
if(client != null)
{
var space = await client.GetSpace();
contentfulLocales = space.Locales;
}
// 1. Validate against Contentful's available locales
if(contentfulLocales.Any(c => c.Code == locale) == false)
{
// Not a supported locale in Contentful, return null to let other providers handle it.
return null;
}
// ... Fallback logic is handled here ...
// 2. Persist the chosen locale to the session
s.Session.SetString(LOCALE_KEY,locale);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Session-based locale persistence
The third provider in the chain ensures that the user's locale choice is remembered across subsequent requests. It runs on every request but only acts if the previous providers did not return a result.
Its logic is simple:
- Check the session for a
FALLBACK_LOCALE_KEY. If present, this means the UI culture must be different from the Contentful culture. This key takes precedence. - If no fallback is present, check for the primary
LOCALE_KEY. - If a culture is found in the session, it returns a
ProviderCultureResult, setting theCultureInfo.CurrentCulturefor the request.
csharp
// File: TheExampleApp/Startup.cs
// Inside the third CustomRequestCultureProvider
var sessionCulture = s.Session.GetString(LOCALE_KEY);
var fallbackCulture = s.Session.GetString(FALLBACK_LOCALE_KEY);
if (!string.IsNullOrEmpty(fallbackCulture))
{
// If a fallback culture is present it should take precedence for the UI.
return await Task.FromResult(new ProviderCultureResult(fallbackCulture, fallbackCulture));
}
if (!string.IsNullOrEmpty(sessionCulture))
{
// Otherwise, use the primary culture from the session.
return await Task.FromResult(new ProviderCultureResult(sessionCulture, sessionCulture));
}
return null; // No culture in session.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
The session value itself is set by two components: the query string provider above, and the LocalesViewComponent, which renders the locale selection dropdown. For more on session usage, see the Session Management documentation.
Fallback chain
A critical feature of the localization system is its ability to handle mismatches between Contentful's supported locales and the application's supported UI locales. For example, Contentful might have content for en-GB (British English), but the application may only have UI translations for en-US.
In this scenario, the system should:
- Fetch content for
en-GBfrom Contentful. - Render the application UI using
en-USstrings.
This is accomplished by "walking the fallback chain" defined in Contentful.
Contentful locale fallbacks
When a requested locale (e.g., en-GB) is not found in the application's SupportedCultures list, the system inspects the locale's configuration fetched from Contentful. It recursively follows the FallbackCode property until it finds a locale that is supported by the application.
| Contentful Locale | FallbackCode |
|---|---|
de-AT | de-DE |
de-DE | en-US |
en-US | null |
If a user requests de-AT, the system will identify de-DE as the appropriate UI fallback, as de-DE is in the application's SupportedCultures list.
Walking the fallback chain
The logic is implemented in the GetNextFallbackLocale recursive helper method within the first custom provider.
csharp
// File: TheExampleApp/Startup.cs
// This logic runs if the requested locale is in Contentful but not in `SupportedCultures`
if(SupportedCultures.Any(c => string.Equals(c.ToString(), locale, StringComparison.OrdinalIgnoreCase)) == false)
{
// The locale is supported in Contentful, but not by the application.
// Walk through the fallback chain to see if we find a supported locale there.
var fallback = GetNextFallbackLocale(contentfulLocales.FirstOrDefault(c => c.Code == locale));
if(fallback != null)
{
// We found a fallback, use that for static strings.
// Store the original locale for Contentful requests.
s.Session.SetString(LOCALE_KEY, locale);
// Store the discovered fallback for UI rendering.
s.Session.SetString(FALLBACK_LOCALE_KEY, fallback);
// Use the fallback for the current request's UI culture.
return await Task.FromResult(new ProviderCultureResult(fallback, fallback));
}
}
// Recursive helper function
string GetNextFallbackLocale(Locale selectedLocale)
{
if(selectedLocale != null && selectedLocale.FallbackCode != null)
{
if(SupportedCultures.Any(c => string.Equals(c.ToString(), selectedLocale.FallbackCode)))
{
// Found a fallback that the app supports.
return selectedLocale.FallbackCode;
}
else
{
// The immediate fallback is not supported, recurse.
return GetNextFallbackLocale(contentfulLocales.FirstOrDefault(c => c.Code == selectedLocale.FallbackCode));
}
}
return null; // No supported fallback found in the entire chain.
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Provider ordering
The order of providers in the RequestCultureProviders list is critical, as they are executed sequentially. The first provider to return a non-null result "wins," and its culture is used for the request.
The application's provider chain is ordered by specificity:
CustomRequestCultureProvider(Query String): This runs first to handle explicit user actions. It has the highest priority, as a user clicking a locale link should immediately change the culture, overriding any session state. It also performs the vital function of setting the session state for subsequent requests.QueryStringRequestCultureProvider(Standard): This is the built-in ASP.NET Core provider. It is included in the list but is effectively superseded by the custom provider above, which also uses thelocalequery key. It remains as a fallback but is unlikely to be executed.CustomRequestCultureProvider(Session): This provider runs on every request where alocalequery parameter is not present. It reads the culture from the session, ensuring the user's choice persists as they navigate the site. Its logic to prioritize theFALLBACK_LOCALE_KEYis crucial for correctly rendering the UI in fallback scenarios.DefaultRequestCulture: If none of the providers in the list can determine a culture (e.g., a new user visiting the site for the first time with no query string or session), the application will use the configuredDefaultRequestCulture, which is"en-US".
This ordering creates a robust and predictable system for managing request culture that aligns with the application's specific integration needs with a headless CMS. For more information on the underlying middleware, see Custom Middleware.