Localization Boundary & Source Generator
PowerPortalsPro pre-fetches every Dataverse-metadata-derived string a route will need before the route's first paint, so users never see the brief flash of fallback key-text that on-demand localization libraries normally produce. The mechanism has three halves — a static default bundle loaded once per session that covers framework + app strings (everything outside tables.* / choices.*), a build-time source generator that emits a per-component manifest of the table and view tokens each component needs, and a runtime LocalizationBoundary component that wraps the route view and fans out parallel per-resource bundle fetches before letting the route render.
The LocalizationBoundary Component
LocalizationBoundary wraps AuthorizeRouteView (or RouteView) inside Routes.razor. On every navigation it reads the current page type's source-generated LocalizationManifest static field, unions the tokens with any BaselinePrefixes the consumer configures, calls IAsyncStringLocalizer.EnsurePrefixesLoadedAsync for the combined token list, and holds children off from rendering until every per-resource bundle has been fetched and merged into the cache. While the fetch is in flight a translucent overlay with a spinner covers the viewport so the previous page's stale text isn't visible.
Baseline Tokens
Most apps don't need to set BaselinePrefixes at all — the source generator detects the per-route table and view tokens from your component code automatically. Use it only for app-known tables.{name} / views.{viewId} tokens that aren't statically detectable (e.g. a layout component that always renders an account picker, where the table is implicit but no TableName="account" attribute appears in the markup the generator scans). Other token shapes are dropped silently by the runtime — the framework's own component strings ship in the static default bundle, so passing components.PowerPortalsPro.* or app.* here is a no-op.
Wiring It Up
The starter template configures LocalizationBoundary for you. If you're integrating it manually, place it inside Routes.razor wrapping the route view, like this:
@using PowerPortalsPro.Web.Blazor.FluentUI.Components
<Router AppAssembly="typeof(MyApp.RouteUrls).Assembly">
<Found Context="routeData">
<LocalizationBoundary RouteData="routeData">
<AuthorizeRouteView RouteData="routeData"
DefaultLayout="typeof(Layout.MainLayout)" />
</LocalizationBoundary>
</Found>
</Router>
The Source Generator and Manifest
The PowerPortalsPro.Localization NuGet package bundles a Roslyn source generator that scans every *.razor and code-behind in the consuming project at build time. For each component (anything inheriting ComponentBase) it emits a partial-class extension carrying a public static readonly LocalizationKeyManifest LocalizationManifest field. The manifest contains the transitive set of tables.{name} and views.{viewId} tokens the component (and every statically-declared child it renders) requests at runtime — each token resolves to a per-resource bundle URL the boundary fetches in parallel before first paint. Strings outside the tables.* / choices.* subtree (app.*, components.*) come from the static default bundle and are deliberately excluded from the manifest.
What the Generator Detects
The scanner picks up the common patterns automatically — no annotations required:
_localizer["tables.foo.…"]indexer accesses are coerced into the owningtables.footoken;_localizer["tables.foo.views.{guid}.…"]accesses become aviews.{guid}token.@inject IStringLocalizer<T>on a razor file lets the generator track which fields the localizer is bound to so indexer accesses on those fields contribute their tokens correctly.GetPrefixedLocalizer("tables.foo")calls have their prefix coerced into the matching table or view token.DefaultViewId/ViewId/ViewIdsattributes on grid components emitviews.{guid}tokens — the server resolves the owning table from the id at request time, so the manifest doesn't need to know it.TableName="name"attributes on grid/record components emit atables.{name}token. The matching per-table bundle returns display names, column metadata, the table's views (and their column overrides), and any global option-set choices the table's columns reference — everything the page could need for that table in one fetch.- Direct child component tags AND
@typeof(SomeComponent)references in attribute values pull the child's manifest into the parent's transitive set, so a page that hosts dynamic components still pre-fetches their tokens.
Inspecting a Manifest
The generated LocalizationManifest field is an ordinary public static — you can inspect it from any C# code or expand it in your debugger. Tokens are sorted deterministically so incremental builds don't churn the generated file.
// Generated by PowerPortalsPro.Localization.SourceGenerators
public partial class Dashboard
{
public static readonly LocalizationKeyManifest LocalizationManifest =
new LocalizationKeyManifest(
"tables.opportunity",
"views.a1b2c3d4-e5f6-4789-abcd-112233445566"
);
}
On-Demand Fallback
Anything the generator can't resolve at compile time — keys built from runtime expressions, new Guid(SomeQualifiedConst) references that cross a project-reference boundary, etc. — falls through to the localizer's on-demand path. A cache miss on a tables.X.… or tables.X.views.Y.… key is coerced into the owning bundle token, queued, debounced briefly, and fetched in the background; LocalizationBoundary remounts the descendant subtree once the fetch completes so stale fallback text gets corrected automatically. Misses on other key shapes (app.*, components.*) are ignored — the static default bundle has had its chance, so a miss means the key truly doesn't exist.
Custom Views Defined in JSON
Custom views you ship in a localization JSON file (e.g. app.en.json) — typically grid views with a hand-picked GUID, defined client-side via CustomViewDefinitions on a grid — are picked up by the per-view bundle endpoint automatically. The bundle service derives the owning table from the key shape (tables.{owningTable}.views.{viewId}.…) rather than the metadata-only savedquery map, so a view that doesn't exist in Dataverse still ships through the manifest. Keys are matched case-insensitively: a JSON file with "28299C6F-EBC0-4206-9E11-A373D4C9891F" (uppercase, the natural way to author a GUID literal) resolves correctly when the source generator emits views.28299c6f-ebc0-4206-9e11-a373d4c9891f from your code.
Note
Reference
PowerPortalsPro.Localizationfrom any project that hosts Blazor components. The package ships both the runtime types (IStringLocalizer,IAsyncStringLocalizer,LocalizationKeyManifest,LocalizationBoundary) and the source generator as an analyzer asset — consumers get manifest emission automatically on the next build with no additional configuration.
LocalizationBoundary Class
Parameters
Name | Type | Default | Description |
|---|---|---|---|
BaselinePrefixes | IEnumerable<string>? | Additional | |
ChildContent | RenderFragment? | The route content to render once localizations are ready. Normally an | |
RouteData | RouteData? | The current route's LocalizationBoundary.RouteData. Derived prefix is |
BaselinePrefixesChildContentRouteDataLocalizationBoundary.RouteData. Derived prefix is 