Lokalisierung
PowerPortalsPro verwendet ein JSON-basiertes Lokalisierungssystem für alle benutzerorientierten Texte. Dazu gehören Komponentenlabels, Tabellen- und Spaltennamen, View-Namen, Validierungsnachrichten und anwendungsspezifische Strings. Der Blazor-Stapel liest sie durch; IStringLocalizerder React-Stapel liest sie durch den useT() Haken. Beide Stacks verbrauchen dieselbe JSON-Form – einmal erstellte app.en.json Strings steuern beide Stacks an (und alle gemeinsam genutzten Ressourcendateien wie Dataverse-Metadata-abgeleitete tables.* Strings fließen automatisch zu beiden).
Lokalisierungsdateien
Die Lokalisierung erfolgt durch JSON-Dateien, die in einem localization Verzeichnis im Serverprojekt platziert werden. Dateien folgen der Namenskonvention name.{culture}.json (z. B. app.en.json, app.fr.json). Die gleiche JSON-Form steuert sowohl den React-Stack (der zur Laufzeit vom Localizer-Anbieter abgerufen wird) als auch den Blazor-Stack (beim Start gelesen von IStringLocalizer).
// app.en.json
{
"app": {
"navigation": {
"home": "Home",
"contacts": "Contacts"
}
}
}
Reagieren
Auf der React-Seite wird der Localizer automatisch von <PowerPortalsProProvider> — er enthält ein, <DefaultLocalizerProvider> der das Bündel des aktiven Ortes von /localizations/... auf der Montage und bei jeder Lokalisierungsänderung abruft. Wickle den Anbieter mit einem <LocaleProvider> Zustand ein, wenn du explizite Standortwechsel möchtest (automatisch erkannt über den URL-Pfad, ASP.NET Kulturcookie oder navigator.language standardmäßig). Zeichenketten, die der Benutzer während des kurzen Fensters vor der Auflösung des Bündels sieht, werden als ihre Rohschlüssel gerendert; Der Anbieter tauscht die echten Zeichenketten sofort ein, sobald der Abruf landet.
// App.tsx — minimaler Bootstrap. PowerPortalsProProvider auto-mounts
// DefaultLocalizerProvider, daher funktioniert useT() in jedem Nachkommen.
import { LocaleProvider, PowerPortalsProProvider } from '@powerportalspro/react';
export function App() {
return (
<LocaleProvider>
<PowerPortalsProProvider>
<Routes />
</PowerPortalsProProvider>
</LocaleProvider>
);
}
Blazor
Registrieren Sie die Lokalisierungsverzeichnisse im Server über Program.cs AddLocalizationDirectory. Das Framework liest sie beim Start und bestätigt jede IStringLocalizer / IStringLocalizer<T> Resolve über DI.
// Program.cs
builder.Services.AddPowerPortalsPro(options =>
{
options.AddLocalizationDirectory("localization");
options.AddLocalizationDirectory("_content/MyApp.Client/localization");
});
HTML-Lokalisierungsdateien
Für längere Inhalte wie E-Mail-Vorlagen können Sie eigenständige HTML-Dateien verwenden, anstatt HTML-Strings in JSON einzubetten. Der Dateiname kodiert den vollständigen Pfad und die Kultur des Lokalisierungsschlüssels und verwendet dabei das Format {key-path}.{culture}.html.
Jedes punktseparierte Segment des Dateinamens entspricht einer Ebene in der Hierarchie des Lokalisierungsschlüssels. Das vorletzte Segment ist der Kulturcode (z. B. en, fr). Platzieren Sie diese Dateien in denselben Lokalisierungsverzeichnissen, die bei registriert sind AddLocalizationDirectory.
Zum Beispiel die folgende Dateistruktur:
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
Die Datei emails.signup-confirmation.body.en.html entspricht dem Lokalisierungsschlüssel emails.signup-confirmation.body der Kultur en . Das entspricht dazu, den HTML-Inhalt als String-Wert in deiner JSON-Datei auf diesem Schlüsselpfad zu haben.
// Rufen Sie den HTML-Inhalt mit dem Lokalisierungsschlüssel ab
var emailBody = _localizer["emails.signup-confirmation.body"];
Rufen Sie den Inhalt mit demselben IStringLocalizer Schlüssel ab, der dem Dateinamen entspricht. Der HTML-Inhalt wird als lokalisierte Zeichenkette zurückgegeben und kann mit gerendert werden ToMarkupString().
Tabellen- und Spaltenetiketten
Tabellen- und Spaltenanzeigen, Beschreibungen und View-Labels werden automatisch aus den Lokalisierungsdateien mithilfe der Konvention tables.{tableName}.label, tables.{tableName}.columns.{columnName}.label, und tables.{tableName}.views.{viewId}.labelaufgelöst.
// 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"
}
}
}
}
}
Anmerkung
Standardmäßig lädt das Framework Metadaten für jede Tabelle in Ihrer Dataverse-Umgebung. Für jedes Portal, das mehrere Sprachen unterstützt, solltest du nur die Tabellen festlegen
LocalizeAllAvailableTables = falseund registrieren, über die du tatsächlich nutztAddTableToLocalize– das Laden jeder Tabelle verlangsamt das Startaufwärmen und füllt den String-Cache mit Labels, die du nie anweist (ein Kostenfaktor pro installierter Sprache). Registrieren Sie jede Tabelle, deren Labels in der Benutzeroberfläche erscheinen (Raster, Formulare, Untergitter, Diagramme), da eine angezeigte Tabelle, die nicht lokalisiert ist, ihre Rohschlüssel anstelle von Labels rendert.account,contact, undadx_externalidentitysind standardmäßig enthalten.
Etiketten anzeigen
Ansichtsbeschriftungen im Rasteransichts-Dropdown-Menü sind unter tables.{tableName}.views.{viewId}.labellokalisiert, wobei {viewId} die GUID der gespeicherten Ansicht von Dataverse ist (ohne Strebe, Kleinbuchstaben). Dies gilt sowohl für die MainGrid Ansichts- als auch SubGrid für Ansichtsselektoren.
// Im 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"
}
}
}
}
}
Anmerkung
Für benutzerdefinierte Ansichten, die über definiert sind
CustomViewDefinitions, gilt dieselbe Konvention – verwenden Sie die GUID der benutzerdefinierten Ansicht als Schlüssel. Wenn kein lokalisiertes Label gefunden wird, wird der Name der Ansicht aus derGridViewDefinitionoder Dataverse-Metadaten als Fallback verwendet.
Spaltenüberschriften anzeigen
Spaltenüberschriften, die in Rastern angezeigt werden, werden mittels eines Rückfallmusters aufgelöst. Das System sucht zunächst nach einer ansichtsspezifischen Spaltenbezeichnung bei tables.{tableName}.views.{viewId}.columns.{columnName}.label. Wenn sie nicht gefunden wird, fällt sie auf das Tabellen-Kolumnenlabel bei tables.{tableName}.columns.{columnName}.labelzurück. Tooltips folgen demselben Muster mit .description anstelle von .label.
Dadurch können Sie die Kopfzeile einer Spalte für eine bestimmte Ansicht überschreiben, ohne deren Beschriftung in anderen Ansichten oder Editoren zu beeinflussen.
// Überschreiben Sie eine Spaltenkopfzeile für eine bestimmte Ansicht
{
"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"
}
}
}
}
}
}
}
Anmerkung
Für Spalten aus verknüpften Entitäten (z. B.
contact.emailaddress1), verwendet der Spaltenname im Schlüssel das Alias-präfixierte Format. Wenn kein ansichtsspezifisches Label gefunden wird, erstellt das System ein Label aus dem verknüpften Spaltennamen und dessen übergeordnetem Spaltenlabel (z. B. "Email (Primary Contact)").
Auswahl- (Optionsset-)Bezeichnungen
Optionslabels der Auswahlspalte werden je nachdem, ob der Optionssatz tabellenbezogen oder global ist, unterschiedlich lokalisiert.
Tabellenbezogene Auswahl
Tabellenbezogene Optionen werden unter tables.{tableName}.choices.{choiceLogicalName}.values.{value}.labellokalisiert. Der logische Name der Wahl wird mit dem Tabellennamen (z. B. account_accountcategorycode) vorangestellt.
// In tables.en.json – tischbezogene Optionen
{
"tables": {
"account": {
"choices": {
"account_accountcategorycode": {
"label": "Category",
"values": {
"1": { "label": "Preferred Customer" },
"2": { "label": "Standard" }
}
}
}
}
}
}
Globale Entscheidungen
Globale Entscheidungen (Optionsmengen, die über mehrere Tabellen geteilt werden) werden auf Wurzelebene unter choices.{choiceLogicalName}.values.{value}.labellokalisiert, außerhalb eines beliebigen Tabellenabschnitts.
// In tables.en.json — globale Auswahlmöglichkeiten (Wurzelebene, außerhalb der "Tabellen")
{
"choices": {
"powerpagelanguages": {
"label": "Preferred Language",
"values": {
"1033": { "label": "English" },
"1036": { "label": "French" },
"1031": { "label": "German" }
}
}
}
}
Anmerkung
Die
ChoiceEditund-KomponentenMultiSelectChoiceEditlösen automatisch Wahllabels von der korrekten Position aus basierend auf derIsGlobalEigenschaft der Spaltenmetadaten auf.
Injizieren des Localizers
Es gibt zwei Möglichkeiten, den Stringlokalisierer einzuschleusen:
IStringLocalizer— Zugriff auf alle Lokalisierungsschlüssel weltweit. Nutze das für Tabellenlabels, geteilte Strings oder wenn du die MethodeGetPrefixedLocalizerbrauchst.IStringLocalizer<T>— Auf einen bestimmten Bauteiltyp zugeschnitten. Schlüssel werden relativ zum Namensraumpfad der Komponente in der JSON-Datei aufgelöst (z. B.components.{Namespace}.{ComponentName}.{key}).
// Global Localizer-Hook — ruft den aktiven Localizer ab und gibt seinen Localizer zurück
// 'nicht' direkt funktionieren. Stabile Identität pro Render; Safe in Effect Deps.
import { useT, usePrefixedT } from '@powerportalspro/react';
function MyComponent() {
const t = useT();
const label = t('app.navigation.home');
// Komponenten-scoped Lookups in React sind Zucker über einem festen Präfix. Die
// Bundle trägt weiterhin 'Components'. <FullTypeName>. <key>'Form —</key></FullTypeName>
// Geben Sie diesen vollständigen Namensraum an usePrefixedT und rufen Sie nur mit dem Schlüssel auf.
const componentT = usePrefixedT(
'components.MyApp.Pages.MyComponent',
);
const title = componentT('title'); // → Komponenten. MyApp.Pages.MyComponent.title
return <h1>{title}</h1>;
}// Globaler Lokalizer – Zugriff auf jeden Schlüssel
[Inject]
private IStringLocalizer _localizer { get; set; } = null!;
// Component-scoped Localizer — Schlüssel lösen sich relativ zum Namensraum der Komponente auf
[Inject]
private IStringLocalizer<MyComponent> _localizer { get; set; } = null!;Komponenten-Scoped Schlüssel
Bei Verwendung IStringLocalizer<T>werden Schlüssel basierend auf dem Namensraum und dem Klassennamen der Komponente aufgelöst. Zum Beispiel löst eine Komponente bei Pages.Editors.TextEdit.TextEditDemoPage Schlüssel aus dem JSON auf components.{AssemblyName}.Pages.Editors.TextEdit.TextEditDemoPage.{key} .
// In app.en.json — Schlüssel für eine Komponente bei Pages/Editors/TextEdit/TextEditDemoPage
{
"components": {
"MyApp.Client": {
"Pages": {
"Editors": {
"TextEdit": {
"TextEditDemoPage": {
"title": "TextEdit",
"description": "A single-line text input."
}
}
}
}
}
}
}
// usePrefixedT pinnt den gesamten Namensraum EINMAL, also ruft die Seiten nur wiederholt
// Der hintere Schlüssel. Entscheidet gegen
// 'Komponenten. MyApp.Client.Seiten.Editors.TextEdit.TextEditDemoPage.title'
// im Bündel.
import { usePrefixedT } from '@powerportalspro/react';
const STRINGS_BASE =
'components.MyApp.Client.Pages.Editors.TextEdit.TextEditDemoPage';
export function TextEditDemoPage() {
const t = usePrefixedT(STRINGS_BASE);
return (
<>
<h1>{t('title')}</h1>
<p dangerouslySetInnerHTML={{ __html: t('description') }} />
</>
);
}<!-- In der Komponente -->
@inject IStringLocalizer<TextEditDemoPage> _localizer
<h1>@_localizer["title"]</h1>
<p>@_localizer["description"].ToMarkupString()</p>Präfixlokalisierer
Nutze GetPrefixedLocalizer es, um einen Sublokalisierer zu erstellen, der automatisch ein Präfix an alle Schlüsselsuche setzt. Dies ist nützlich, um wiederholende Schlüsselpräfixe in Komponenten zu vermeiden, die viele Schlüssel aus demselben Abschnitt verwenden.
// usePrefixedT(prefix) gibt eine t-Funktion zurück, wobei jeder Tast automatisch präpended ist
// mit '${Präfix}.'. Gespeichert pro Präfixwert, damit es stabil ist
// Renders (Safe in Effect Deps).
import { usePrefixedT } from '@powerportalspro/react';
function Nav() {
const t = usePrefixedT('app.navigation');
const home = t('home'); // → app.navigation.home
const contacts = t('contacts'); // → app.navigation.contacts
return /* ... */;
}// Ohne Präfix – repetitiv
var home = _localizer["app.navigation.home"];
var contacts = _localizer["app.navigation.contacts"];
// Mit Präfix — cleaner
var navLocalizer = _localizer.GetPrefixedLocalizer("app.navigation");
var home = navLocalizer["home"];
var contacts = navLocalizer["contacts"];HTML in lokalisierten Zeichenketten
Lokalisierte Zeichenketten können HTML-Markup enthalten. Verwenden Sie die Erweiterungsmethode ToMarkupString() , um sie wie MarkupString in Razor-Vorlagen zu rendern.
// dangerouslySetInnerHTML rendert die lokalisierte Zeichenkette als HTML — die
// Der Wert ist ein vertrauenswürdiger Aufschlag, da du ihn im Bundle verfasst hast.
<p dangerouslySetInnerHTML={{ __html: t('description') }} /><!-- Rendert HTML-Markup aus einer lokalisierten Zeichenkette -->
<p>@_localizer["description"].ToMarkupString()</p>Das beste Match finden
Suchen FindLocalizedString Sie den ersten passenden Schlüssel aus einer Liste von Kandidaten nach. Das ist nützlich für Rückfallmuster, bei denen du zuerst eine bestimmte Taste ausprobieren und dann auf eine allgemeinere zurückgreifen möchtest.
// 't()' gibt die rohe Taste zurück, wenn eine Zeichenkette fehlt, also 't(Schlüssel) !== Schlüssel'
// Ist die "Hat dieser Schlüssel gelöst?"-Kontrolle, auf der du einen Rückfall-Walk baust?
import { useT } from '@powerportalspro/react';
function resolve(t: ReturnType<typeof useT>, ...candidates: string[]): string {
for (const key of candidates) {
const value = t(key);
if (value !== key) return value;
}
return candidates[candidates.length - 1];
}
const t = useT();
const label = resolve(
t,
`tables.${tableName}.columns.${columnName}.label`,
`tables.${tableName}.label`,
);// Versuche zuerst eine bestimmte Tonart und greife dann auf den Allgemeinen zurück
var label = _localizer.FindLocalizedString(
$"tables.{tableName}.columns.{columnName}.label",
$"tables.{tableName}.label");Auf der React-Seite useT() gibt er die rohe Taste zurück, wenn ein String fehlt – t(key) !== key ist die "Hat dieser Schlüssel aufgelöst?"-Check, auf dem du einen Fallback-Walk baust. Wickle das Muster in einen Helfer, wenn du es an mehreren Einsatzorten brauchst.
Argumentinterpolation
Strings können positionelle {0}, {1}, ... Platzhalter, die zur Suchzeit ersetzt werden. Blazor übergibt Args als Params-Array an IStringLocalizer; React übergibt sie als zweites Argument zu t(). Optionale Formatspezifikationen innerhalb eines Platzhalters ({0:N0}, {1:yyyy-MM-dd}) werden nur von Blazors String.Format Pipeline anerkannt; für React wird mit Intl.NumberFormat / Intl.DateTimeFormat vor der Weitergabe des Wertes formatiert.
// Im app.en.json
{
"app": {
"welcome": "Welcome back, {0}! You have {1} unread messages."
}
}
// Args ging als zweites Argument zu t() durch – positional, indexiert durch '{0}'.
const greeting = t('app.welcome', [userName, unreadCount]);// Args wurden über das Params-Array weitergegeben – der Indexer von IStringLocalizer akzeptiert sie.
var greeting = _localizer["app.welcome", userName, unreadCount];Eifriges Laden (React)
Bei einem Cache-Miss holt der React-Stack faul das besitzende pro-Ressource-Bundle (ein debounced Batch pro ~75 ms) und rendert die verbrauchende Komponente erneut, wenn die Strings landen. Um den kurzen Flash von Rohtasten zu vermeiden, deklarieren Sie die Präfixe, die eine Seite benötigt, im Voraus über useLocalization([...]), oder verschieben Sie das Rendern, bis sie über den höherwertigen <LocalizationBoundary> Wrapper aufgelöst werden. Siehe die Seite LocalizationBoundary für das vollständige Muster.
// Laden Sie die Pakete pro Ressource einer Seite von Anfang an, um eine
// Ein Flash von Rohtasten beim Navigieren. Tokens, die bereits vorhanden sind
// geholt (oder im Flug) werden still dedupt, also rufen sie von zwei aus
// Komponenten auf derselben Seite ergeben insgesamt eine Hin- und Rückreise.
import { useLocalization } from '@powerportalspro/react';
function AccountFormPage() {
useLocalization(['tables.account', 'tables.contact']);
return /* ... Form... */;
}
IStringLocalizer Interface
Eigenschaften
Name | Typ | Default | Beschreibung |
|---|---|---|---|
Item | LocalizedString |
ItemMethoden
Name | Parameter | Typ | Beschreibung |
|---|---|---|---|
FindLocalizedString | string[] keys | LocalizedString | Gibt das erste gültige Match basierend auf den bereitgestellten Schlüsseln zurück. |
GetAllStrings | bool includeParentCultures | IEnumerable<LocalizedString> | |
GetPrefixedLocalizer | string prefix | IPrefixedStringLocalizer | Gibt ein neues Localization.IPrefixedStringLocalizer zurück, das allen Schlüsselnachschlagungen das gegebene Präfix voranstellt. |
FindLocalizedStringGetAllStringsGetPrefixedLocalizerLocalization.IPrefixedStringLocalizer zurück, das allen Schlüsselnachschlagungen das gegebene Präfix voranstellt.