Localization Boundary & Source Generator
PowerPortalsPro pre-fetches every localization key 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 two halves — a build-time source generator that emits a per-component manifest of the keys each component needs, and a runtime LocalizationBoundary component that wraps the route view and uses that manifest to issue a single batched HTTP fetch for the keys before letting the route render.
The LocalizationBoundary Component
LocalizationBoundary wraps AuthorizeRouteView (or RouteView) inside Routes.razor. On every navigation it derives a key-prefix from RouteData.PageType, unions it with any BaselinePrefixes the consumer configures, calls IAsyncStringLocalizer.EnsurePrefixesLoadedAsync for the combined token list, and holds children off from rendering until the fetch completes. 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 Prefixes
Some keys are needed on every page — layout strings, common buttons, framework component labels. Pass these as BaselinePrefixes so they're included in every per-route fetch (the localizer dedupes already-loaded prefixes, so the cost is paid once per session). LocalizationBaselines.Default ships a curated set covering the framework's components.PowerPortalsPro.Web.Blazor.* namespaces; concatenate it with your app's own cross-cutting prefixes (e.g. app) when you wire the boundary up.
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"
BaselinePrefixes="_baselinePrefixes">
<AuthorizeRouteView RouteData="routeData"
DefaultLayout="typeof(Layout.MainLayout)" />
</LocalizationBoundary>
</Found>
</Router>
@code {
private readonly IReadOnlyList<string> _baselinePrefixes =
LocalizationBaselines.Default.Concat(new[] { "app" }).ToArray();
}
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 localization keys the component (and every statically-declared child it renders) requests at runtime — so when LocalizationBoundary issues its pre-fetch, the server has the complete list of strings the page is about to need.
What the Generator Detects
The scanner picks up the common patterns automatically — no annotations required:
_localizer["foo"]indexer accesses on anyIStringLocalizer/IAsyncStringLocalizer/IPrefixedStringLocalizer-typed field.@inject IStringLocalizer<T>on a razor file emits the broadercomponents.{T.FullName}prefix (covers every key the typed localizer can ask for, including dynamic ones).GetPrefixedLocalizer("app.foo")calls emit the literal prefix as a token.DefaultViewId/ViewId/ViewIdsattributes on grid components emittables.[*].views.{guid}wildcard tokens — the server resolves the wildcard against any table since view ids are globally unique in Dataverse.TableName="name"attributes on grid/record components emit a broadtables.{name}prefix covering display names, column metadata, view labels, and choice values for that table.- 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 keys.
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. The keys 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(
"components.MyApp.Pages.Demos.Dashboard",
"components.MyApp.Pages.Demos.DashboardMainDemo",
"tables.[*].views.a1b2c3d4-e5f6-4789-abcd-112233445566",
"tables.opportunity"
);
}
On-Demand Fallback
Anything the generator can't resolve at compile time — keys built from runtime expressions, new Guid(SomeQualifiedConst) references, etc. — falls through to the localizer's on-demand path. Cache misses queue the missing key, debounce briefly, then issue a batched fetch in the background; LocalizationBoundary remounts the descendant subtree once the fetch completes so stale fallback text gets corrected automatically.
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.
