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. The Blazor stack reads them through IStringLocalizer; the React stack reads them through the useT() hook. Both stacks consume the SAME JSON shape — strings authored once in app.en.json drive both stacks (and any shared resource files such as Dataverse-metadata-derived tables.* strings flow through to both automatically).

Localization Files

Localization is driven by JSON files placed in a localization directory in the server project. Files follow the naming convention name.{culture}.json (e.g. app.en.json, app.fr.json). The same JSON shape drives both the React stack (fetched at runtime by the localizer provider) and the Blazor stack (read at startup by IStringLocalizer).

React

On the React side the localizer is auto-mounted by <PowerPortalsProProvider> — it includes a <DefaultLocalizerProvider> that fetches the active locale's bundle from /localizations/... on mount and on every locale change. Wrap the provider with a <LocaleProvider> when you want explicit locale-switching state (auto-detected from the URL path, ASP.NET culture cookie, or navigator.language by default). Strings the user sees during the brief window before the bundle resolves render as their raw keys; the provider swaps in the real strings as soon as the fetch lands.

Blazor

Register the localization directories in the server's Program.cs via AddLocalizationDirectory. The framework reads them at startup, and they back every IStringLocalizer / IStringLocalizer<T> resolved through DI.

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:

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 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.

Note

By default the framework loads metadata for every table in your Dataverse environment. For any portal that supports multiple languages, you should set LocalizeAllAvailableTables = false and register only the tables you actually use via AddTableToLocalize — loading every table slows the startup warmup and fills the string cache with labels you never display (a cost paid per installed language). Register every table whose labels appear in the UI (grids, forms, subgrids, charts), since a displayed table that isn't localized renders its raw keys instead of labels. account, contact, and adx_externalidentity are included by default.

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.

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 the GridViewDefinition or 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.

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).

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.

Note

The ChoiceEdit and MultiSelectChoiceEdit components automatically resolve choice labels from the correct location based on the IsGlobal property 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 the GetPrefixedLocalizer method.
  • 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}).
React
Blazor

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.

React
Blazor

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.

React
Blazor

HTML in Localized Strings

Localized strings can contain HTML markup. Use the ToMarkupString() extension method to render them as MarkupString in Razor templates.

React
Blazor

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.

React
Blazor

On the React side, useT() returns the raw key when a string is missing — t(key) !== key is the "did this key resolve?" check you build a fallback walk on. Wrap the pattern in a helper when you need it at multiple call sites.

Argument Interpolation

Strings can include positional {0}, {1}, … placeholders that are substituted at lookup time. Blazor passes args as a params array to IStringLocalizer; React passes them as the second argument to t(). Optional format specifiers inside a placeholder ({0:N0}, {1:yyyy-MM-dd}) are honored by Blazor's String.Format pipeline only; for React, pre-format with Intl.NumberFormat / Intl.DateTimeFormat before passing the value in.

React
Blazor

Eager Loading (React)

On a cache miss the React stack lazily fetches the owning per-resource bundle (one debounced batch per ~75 ms) and re-renders the consuming component when the strings land. To eliminate the brief flash of raw keys, declare the prefixes a page will need up front via useLocalization([...]), or hold off rendering until they resolve via the higher-level <LocalizationBoundary> wrapper. See the LocalizationBoundary page for the full pattern.

IStringLocalizer Interface

Properties

Name
Type
Default
Description
ItemLocalizedString
Name: Item
Type: LocalizedString

Methods

Name
Parameters
Type
Description
FindLocalizedStringstring[] keys
LocalizedString
Returns the first valid match based on the provided keys.
GetAllStringsbool includeParentCultures
IEnumerable<LocalizedString>
GetPrefixedLocalizerstring prefix
IPrefixedStringLocalizer
Returns a new Localization.IPrefixedStringLocalizer that prepends the given prefix to all key lookups.
Name: FindLocalizedString
Parameters: string[] keys
Type: LocalizedString
Description: Returns the first valid match based on the provided keys.
Name: GetAllStrings
Parameters: bool includeParentCultures
Type: IEnumerable<LocalizedString>
Name: GetPrefixedLocalizer
Parameters: string prefix
Type: IPrefixedStringLocalizer
Description: Returns a new Localization.IPrefixedStringLocalizer that prepends the given prefix to all key lookups.