Appearance
Are you an LLM? You can read better optimized documentation at /configuration/localization.md for this page in Markdown format
Localization
This document provides a detailed technical overview of the internationalization (i18n) and localization (l10n) architecture of the application. It covers the configuration, custom implementations, and workflows required for managing and extending language support.
Supported cultures
The application's user interface (UI) supports a specific set of languages, which are configured in Startup.cs. This list dictates which static translation files the application will recognize and load.
The supported cultures are defined in a static List<CultureInfo>:
csharp
// File: TheExampleApp/Startup.cs
public static List<CultureInfo> SupportedCultures = new List<CultureInfo>
{
//When adding supported locales make sure to also add a static translation files for the locale under /wwwroot/locales
new CultureInfo("en-US"),
new CultureInfo("de-DE"),
};1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
This list is passed to the RequestLocalizationOptions during application startup to configure ASP.NET Core's localization middleware. It is crucial that any culture added to this list has a corresponding JSON translation file.
For more details on service configuration, see /configuration/services.md.
JSON localization files
Unlike the standard ASP.NET Core approach using .resx files, this application employs a custom solution that sources UI translations from JSON files. This method simplifies the translation workflow and aligns well with modern development practices.
Location and Naming Convention:
- All locale files are located in the
TheExampleApp/wwwroot/locales/directory. - Each file must be named according to its culture code, matching an entry in the
SupportedCultureslist. For example, the cultureen-UScorresponds to the fileen-US.json.
Structure: The JSON files contain a flat key-value structure, where the key is the translation identifier used in the code and the value is the translated string.
json
// File: TheExampleApp/wwwroot/locales/en-US.json
{
"defaultTitle": "The Example App",
"whatIsThisApp": "Help",
"viewOnGithub": "View on GitHub",
"settingsLabel": "Settings",
"homeLabel": "Home",
"coursesLabel": "Courses",
...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
json
// File: TheExampleApp/wwwroot/locales/de-DE.json
{
"defaultTitle": "The Example App",
"whatIsThisApp": "Hilfe",
"viewOnGithub": "Auf GitHub ansehen",
"settingsLabel": "Einstellungen",
"homeLabel": "Startseite",
"coursesLabel": "Kurse",
...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
These files are embedded as resources in the application assembly and loaded via an EmbeddedFileProvider, which is registered as a singleton IFileProvider in Startup.cs.
JsonViewLocalizer
The core of the JSON-based localization system is the JsonViewLocalizer class, a custom implementation of the IViewLocalizer interface. This class is responsible for loading the JSON files into memory and providing translations to the Razor views.
Implementation Details: The JsonViewLocalizer is registered as a singleton service in Startup.cs:
csharp
// File: TheExampleApp/Startup.cs
services.AddSingleton<IViewLocalizer, JsonViewLocalizer>();1
2
3
2
3
Its constructor uses an IFileProvider to access the wwwroot/locales directory, iterates through the JSON files, and deserializes each one into a dictionary. These dictionaries are stored in a parent dictionary, keyed by the locale code (e.g., "en-US", "de-DE").
csharp
// File: TheExampleApp/Configuration/LocalizationConfiguration.cs
public class JsonViewLocalizer : IViewLocalizer
{
private Dictionary<string, Dictionary<string,string>> _items = new Dictionary<string, Dictionary<string, string>>();
public JsonViewLocalizer(IFileProvider provider)
{
var files = provider.GetDirectoryContents("");
foreach (IFileInfo file in files)
{
using ( var sr = new StreamReader(file.CreateReadStream()))
{
var fileContents = sr.ReadToEnd();
var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(fileContents);
var fileName = Path.GetFileNameWithoutExtension(file.Name);
// Extract the culture code from the file name (e.g., 'en-US' from 'en-US.json').
_items.Add(fileName.Substring(fileName.LastIndexOf('.') + 1), dictionary);
}
}
}
// ... other methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Usage and Gotchas: In Razor views, translations are accessed via the injected @Localizer service (e.g., @Localizer["homeLabel"]). This invokes the indexer of JsonViewLocalizer:
csharp
// File: TheExampleApp/Configuration/LocalizationConfiguration.cs
public LocalizedHtmlString this[string name] => new LocalizedHtmlString(name, _items[CultureInfo.CurrentCulture.ToString()][name]);1
2
3
2
3
Warning: This indexer performs a direct dictionary lookup. If the current culture is not loaded or a translation key is missing from that culture's JSON file, it will throw a
KeyNotFoundExceptionat runtime, breaking the page render.
However, when accessing translations programmatically via the GetString method, the implementation provides graceful error handling:
csharp
// File: TheExampleApp/Configuration/LocalizationConfiguration.cs
public LocalizedString GetString(string name, params object[] arguments)
{
var culture = CultureInfo.CurrentCulture.ToString();
if (!_items.ContainsKey(culture) || !_items[culture].ContainsKey(name))
{
return new LocalizedString(name, name, true); // Returns key as value with resourceNotFound flag
}
return new LocalizedString(name, _items[culture][name]);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
When using GetString, if a culture or key is missing, it returns a LocalizedString with the key name as the value and the resourceNotFound flag set to true, avoiding runtime exceptions. Despite this, it is still critical to ensure all keys exist across all JSON locale files to maintain consistency.
Request culture providers
The application determines the active culture for each request using a custom chain of providers configured in Startup.cs. This chain allows for sophisticated logic that integrates with both the session and the Contentful backend.
The providers are executed in the following order:
Custom Provider (Query String & Contentful Logic):
- This is the primary mechanism for changing locales. It activates when a
?locale=<culture_code>parameter is present in the URL. - It validates the requested locale against the list of locales available in the Contentful space. This ensures that the application doesn't attempt to request content for a language that Contentful doesn't support. See /architecture/contentful_integration.md for more on this integration.
- It handles the Locale Fallback Chain if the requested locale is not directly supported by the application's UI.
- Upon successful validation, it stores the chosen locale in the user's session.
- This is the primary mechanism for changing locales. It activates when a
QueryStringRequestCultureProvider:- A standard ASP.NET Core provider that also looks for the
localequery string key. It serves as a simpler, secondary mechanism.
- A standard ASP.NET Core provider that also looks for the
Custom Provider (Session Logic):
- This provider ensures locale persistence across requests.
- It reads the culture from the
HttpContext.Session. - Crucially, it first checks for a
FALLBACK_LOCALE_KEYand uses it if present. Otherwise, it uses the primaryLOCALE_KEY. This allows the UI and Contentful requests to use different locales when a fallback is active.
csharp
// File: TheExampleApp/Startup.cs
app.UseRequestLocalization(new RequestLocalizationOptions
{
RequestCultureProviders = new List<IRequestCultureProvider>
{
// 1. Custom provider for query string, Contentful validation, and fallback logic
new CustomRequestCultureProvider(async (s) => { /* ... */ }),
// 2. Standard query string provider
new QueryStringRequestCultureProvider()
{
QueryStringKey = LOCALE_KEY
},
// 3. Custom provider to read from the session
new CustomRequestCultureProvider(async (s) => { /* ... */ })
},
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Locale fallback chain
A key architectural feature is the ability to display content from Contentful for a specific locale while rendering the application's static UI in a different, supported fallback language.
Scenario: Imagine the Contentful space supports Spanish (es-ES) and has it configured to fall back to English (en-US). The application itself, however, does not have an es-ES.json file for its UI.
Execution Flow:
- A user navigates to
/?locale=es-ES. - The first
CustomRequestCultureProviderdetects thelocaleparameter. - It validates that
es-ESis a valid locale in Contentful. - It then checks if
es-ESis in the application'sSupportedCultureslist. It is not. - The provider then inspects the fallback chain defined in Contentful for
es-ES. It finds thates-ESfalls back toen-US. - It checks if
en-USis in theSupportedCultureslist. It is. - The provider then performs two actions:
- It sets the session key
LOCALE_KEYto"es-ES". This ensures that all subsequent API calls to Contentful will request Spanish content. - It sets the session key
FALLBACK_LOCALE_KEYto"en-US". - It sets the current request's UI culture to
en-US.
- It sets the session key
The result is a seamless experience where the user sees Spanish content from the CMS rendered within an English application chrome. This logic is implemented in the GetNextFallbackLocale helper method within Startup.cs.
Session-based locale storage
The user's selected locale is persisted throughout their session to provide a consistent language experience without requiring a query parameter on every request.
Two session keys are used:
public const string LOCALE_KEY = "locale";- Stores the primary locale requested by the user (e.g.,
es-ES). This value is used by theContentfulClientto fetch localized content.
- Stores the primary locale requested by the user (e.g.,
public const string FALLBACK_LOCALE_KEY = "fallback-locale";- Stores the fallback UI locale (e.g.,
en-US) only when the primary locale is not supported by the application's JSON files.
- Stores the fallback UI locale (e.g.,
The LocalesViewComponent is responsible for rendering the language selection dropdown. It reads the session to determine the current selection and fetches the list of available locales from Contentful.
csharp
// File: TheExampleApp/ViewComponents/LocalesViewComponent.cs
public async Task<IViewComponentResult> InvokeAsync()
{
// ... (fetches space locales from Contentful)
// Reads the primary locale from the session, defaulting to the current culture.
var selectedLocale = HttpContext.Session.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString();
var localeInfo = new LocalesInfo
{
Locales = space.Locales,
SelectedLocale = space?.Locales.FirstOrDefault(c => c.Code == selectedLocale) ?? space?.Locales.Single(c => c.Default),
};
// Ensures the session is set for subsequent requests.
HttpContext.Session.SetString(Startup.LOCALE_KEY, localeInfo.SelectedLocale?.Code);
return View(localeInfo);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Adding a new language
Follow these steps to add support for a new language (e.g., French, fr-FR) to the application UI.
Configure in Contentful:
- Navigate to your Contentful space settings.
- Under "Locales", add the new locale (
fr-FR). - It is highly recommended to configure a fallback language (e.g.,
en-US). - For more details, see the Contentful Integration documentation.
Update Application Configuration:
- Open
TheExampleApp/Startup.cs. - Add the new culture to the
SupportedCultureslist.
csharp// File: TheExampleApp/Startup.cs public static List<CultureInfo> SupportedCultures = new List<CultureInfo> { new CultureInfo("en-US"), new CultureInfo("de-DE"), new CultureInfo("fr-FR"), // Add the new culture };1
2
3
4
5
6
7
8- Open
Create JSON File:
- In the
TheExampleApp/wwwroot/locales/directory, create a new file namedfr-FR.json.
- In the
Translate Strings:
- Copy the entire contents of
TheExampleApp/wwwroot/locales/en-US.jsoninto the newfr-FR.jsonfile. - Translate the value for each key into French. Do not change the keys.
- Copy the entire contents of
Build and Test:
- Rebuild and run the application.
- The new language should now appear in the locale dropdown. Select it to verify that the UI strings have been translated.
Translation workflow
To maintain consistency and prevent runtime errors, adhere to the following workflow when managing UI translations.
- Source of Truth: The
en-US.jsonfile should be considered the canonical source for all translation keys. - Adding a New String:
- When a new UI string is needed, first add the key and its English translation to
en-US.json. - Immediately after, add the same key to all other locale files (e.g.,
de-DE.json,fr-FR.json). - Provide the translation for the new key in each file. If a translation is not yet available, you can temporarily use the English string as a placeholder, but the key must exist.
- When a new UI string is needed, first add the key and its English translation to
- Preventing Errors: As noted in the JsonViewLocalizer section, a missing key in any active locale file will cause a
KeyNotFoundException. A disciplined workflow of keeping all JSON files structurally synchronized is essential for application stability. For a more robust system, consider modifying theJsonViewLocalizerindexer to fall back to a default string if a key is not found.