Localization
PowerPortalsPro uses a JSON-based localization system for all user-facing text. This includes component labels, table and column names, view names, validation messages, and application-specific strings.
Localization Files
Localization is driven by JSON files placed in a localization directory. Register localization directories in Program.cs using AddLocalizationDirectory. Files follow the naming convention name.{culture}.json (e.g. app.en.json, app.fr.json).
// Program.cs
builder.Services.AddPowerPortalsPro(options =>
{
options.AddLocalizationDirectory("localization");
options.AddLocalizationDirectory("_content/MyApp.Client/localization");
});
// app.en.json
{
"app": {
"navigation": {
"home": "Home",
"contacts": "Contacts"
}
}
}
HTML Localization Files
For longer content such as email templates, you can use standalone HTML files instead of embedding HTML strings in JSON. The file name encodes the full localization key path and culture, using the format {key-path}.{culture}.html.
Each dot-separated segment of the file name maps to a level in the localization key hierarchy. The second-to-last segment is the culture code (e.g. en, fr). Place these files in the same localization directories registered with AddLocalizationDirectory.
For example, the following file structure:
localization/
app.en.json
app.fr.json
emails.signup-confirmation.body.en.html
emails.signup-confirmation.body.fr.html
emails.password-reset.body.en.html
The file emails.signup-confirmation.body.en.html maps to the localization key emails.signup-confirmation.body for the en culture. This is equivalent to having the HTML content as a string value in your JSON file at that key path.
// Retrieve the HTML content using the localization key
var emailBody = _localizer["emails.signup-confirmation.body"];
Retrieve the content using the same IStringLocalizer key that corresponds to the file name. The HTML content is returned as a localized string and can be rendered with ToMarkupString().
Table & Column Labels
Table and column display names, descriptions, and view labels are automatically resolved from the localization files using the convention tables.{tableName}.label, tables.{tableName}.columns.{columnName}.label, and tables.{tableName}.views.{viewId}.label.
// tables.en.json
{
"tables": {
"account": {
"label": "Account",
"collectionLabel": "Accounts",
"columns": {
"name": {
"label": "Account Name",
"description": "The name of the account."
}
},
"views": {
"00000000-0000-0000-0000-000000000001": {
"label": "Active Accounts"
}
}
}
}
}
View Labels
View labels in the grid view selector dropdown are localized under tables.{tableName}.views.{viewId}.label, where {viewId} is the GUID of the Dataverse saved view (without braces, lowercase). This applies to both MainGrid and SubGrid view selectors.
// In tables.en.json
{
"tables": {
"account": {
"views": {
"00000000-0000-0000-00aa-000010001001": {
"label": "Active Accounts"
},
"00000000-0000-0000-00aa-000010001002": {
"label": "Inactive Accounts"
},
"91732ad4-b4fe-49ff-80cd-72b280eff088": {
"label": "All Contacts & Accounts"
}
}
}
}
}
Note
For custom views defined via
CustomViewDefinitions, the same convention applies — use the custom view's GUID as the key. If no localized label is found, the view's name from theGridViewDefinitionor Dataverse metadata is used as a fallback.
View Column Headers
Column headers displayed in grids are resolved using a fallback pattern. The system first looks for a view-specific column label at tables.{tableName}.views.{viewId}.columns.{columnName}.label. If not found, it falls back to the table-level column label at tables.{tableName}.columns.{columnName}.label. Tooltips follow the same pattern using .description instead of .label.
This allows you to override a column's header for a specific view without affecting its label in other views or editors.
// Override a column header for a specific view
{
"tables": {
"account": {
"views": {
"00000000-0000-0000-00aa-000010001001": {
"label": "Active Accounts",
"columns": {
"name": {
"label": "Company",
"description": "The company name for this account."
},
"contact.emailaddress1": {
"label": "Contact Email"
}
}
}
}
}
}
}
Note
For columns from linked entities (e.g.
contact.emailaddress1), the column name in the key uses the alias-prefixed format. If no view-specific label is found, the system constructs a label from the linked column name and its parent column label (e.g. "Email (Primary Contact)").
Choice (Option Set) Labels
Choice column option labels are localized differently depending on whether the option set is table-scoped or global.
Table-Scoped Choices
Table-scoped choices are localized under tables.{tableName}.choices.{choiceLogicalName}.values.{value}.label. The choice logical name is prefixed with the table name (e.g. account_accountcategorycode).
// In tables.en.json — table-scoped choices
{
"tables": {
"account": {
"choices": {
"account_accountcategorycode": {
"label": "Category",
"values": {
"1": { "label": "Preferred Customer" },
"2": { "label": "Standard" }
}
}
}
}
}
}
Global Choices
Global choices (option sets shared across multiple tables) are localized at the root level under choices.{choiceLogicalName}.values.{value}.label, outside of any table section.
// In tables.en.json — global choices (root level, outside "tables")
{
"choices": {
"powerpagelanguages": {
"label": "Preferred Language",
"values": {
"1033": { "label": "English" },
"1036": { "label": "French" },
"1031": { "label": "German" }
}
}
}
}
Note
The
ChoiceEditandMultiSelectChoiceEditcomponents automatically resolve choice labels from the correct location based on theIsGlobalproperty of the column metadata.
Injecting the Localizer
There are two ways to inject the string localizer:
IStringLocalizer— Access all localization keys globally. Use this for table labels, shared strings, or when you need theGetPrefixedLocalizermethod.IStringLocalizer<T>— Scoped to a specific component type. Keys are resolved relative to the component's namespace path in the JSON file (e.g.components.{Namespace}.{ComponentName}.{key}).
// Global localizer — access any key
[Inject]
private IStringLocalizer _localizer { get; set; } = null!;
// Component-scoped localizer — keys resolve relative to the component's namespace
[Inject]
private IStringLocalizer<MyComponent> _localizer { get; set; } = null!;
Component-Scoped Keys
When using IStringLocalizer<T>, keys are resolved based on the component's namespace and class name. For example, a component at Pages.Editors.TextEdit.TextEditDemoPage resolves keys from components.{AssemblyName}.Pages.Editors.TextEdit.TextEditDemoPage.{key} in the JSON.
// In app.en.json — keys for a component at Pages/Editors/TextEdit/TextEditDemoPage
{
"components": {
"MyApp.Client": {
"Pages": {
"Editors": {
"TextEdit": {
"TextEditDemoPage": {
"title": "TextEdit",
"description": "A single-line text input."
}
}
}
}
}
}
}
<!-- In the component -->
@inject IStringLocalizer<TextEditDemoPage> _localizer
<h1>@_localizer["title"]</h1>
<p>@_localizer["description"].ToMarkupString()</p>
Prefixed Localizer
Use GetPrefixedLocalizer to create a sub-localizer that automatically prepends a prefix to all key lookups. This is useful for avoiding repetitive key prefixes in components that use many keys from the same section.
// Without prefix — repetitive
var home = _localizer["app.navigation.home"];
var contacts = _localizer["app.navigation.contacts"];
// With prefix — cleaner
var navLocalizer = _localizer.GetPrefixedLocalizer("app.navigation");
var home = navLocalizer["home"];
var contacts = navLocalizer["contacts"];
HTML in Localized Strings
Localized strings can contain HTML markup. Use the ToMarkupString() extension method to render them as MarkupString in Razor templates.
<!-- Renders HTML markup from localized string -->
<p>@_localizer["description"].ToMarkupString()</p>
Finding the Best Match
Use FindLocalizedString to look up the first matching key from a list of candidates. This is useful for fallback patterns where you want to try a specific key first, then fall back to a more general one.
// Try specific key first, fall back to general
var label = _localizer.FindLocalizedString(
$"tables.{tableName}.columns.{columnName}.label",
$"tables.{tableName}.label");
IStringLocalizer Class
Properties
Name | Type | Default | Description |
|---|---|---|---|
Item | LocalizedString |
ItemMethods
Name | Parameters | Type | Description |
|---|---|---|---|
FindLocalizedString | string[] keys | LocalizedString | Returns the first valid match based on the provided keys. |
GetAllStrings | bool includeParentCultures | IEnumerable<LocalizedString> | |
GetPrefixedLocalizer | string prefix | IPrefixedStringLocalizer | Returns a new that prepends the given prefix to all key lookups. |
FindLocalizedStringGetAllStringsGetPrefixedLocalizer