eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsArchitecture
Localisation
Edit this page
13 min read
Le multilingue dans eQuantic.UI suit une seule loi : qui développe localise avec ce que .NET lui-même offre : .resx, l'accesseur fortement typé, CultureInfo. Il ne voit jamais un catalogue JavaScript ni un DSL du framework. La conception complète (Track L : comment eqc ponte ResourceManager vers le navigateur, les catalogues par culture, le sous-ensemble de formatage, les limites honnêtes) vit dans le dépôt, dans docs/I18N-PLAN.md. Cette page documente ce qui est livré aujourd'hui et ce qui reste de la conception.
Livré
Les chaînes propres au SDK vivent derrière une seule couture
Depuis 0.2.0-preview.24
Les composants ne codent jamais en dur une chaîne destinée à l'utilisateur. Chaque libellé intégré (les annonces de bascule qu'entend un lecteur d'écran ("Checked", "On"), les affordances de fermeture, le placeholder de recherche, la barre de recherche de l'éditeur de code) lit SdkStrings, une classe statique de propriétés (jamais de constantes : une constante traverse les assemblys par VALEUR, et des sites d'appel inlinés continueraient d'annoncer la langue de la machine de build après que la couture a appris les cultures). Cette couture est adossée au resx aujourd'hui : chaque propriété lit SdkResources, un accesseur Designer ordinaire, et aucun composant n'a changé quand c'est arrivé. Sur le web, eqc réécrit l'accesseur en une recherche dans le catalogue de culture que le shell inline, si bien que la même propriété répond depuis un assembly satellite sur le serveur et depuis du JSON dans le navigateur.
Une application avec zéro resx à elle est déjà localisée par cela : les clés du SDK rejoignent le catalogue de chaque culture que l'application émet, donc un lecteur d'écran entend "Marcado" sur une page pt-BR que personne n'a traduite.
Quelles langues sont livrées, et comment en livrer moins. Les chaînes du SDK sont traduites en pt-BR et en es aujourd'hui, et elles voyagent DANS le paquet sous forme d'assemblys satellites (lib/net10.0/<culture>/eQuantic.UI.Components.resources.dll, environ 8 Ko chacun) : un paquet qui porte ses langues, comme Roslyn et la plateforme de test livrent les leurs, plutôt qu'un paquet par langue. Une application qui n'en veut que certaines n'a pas besoin d'un paquet différent, parce que .NET possède déjà cette décision :
1
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
Mesuré sur un consommateur du paquet publié : sans elle, la sortie de l'application porte es/ et pt-BR/ ; avec elle, aucun des deux. Le seul endroit où cela ne s'applique pas est une référence de PROJET : les exemples de ce dépôt gardent les satellites dans les deux cas, parce que la propriété filtre ce qui arrive par un paquet.
Les fenêtres Photon adoptent la locale de la machine au lancement
Depuis 0.2.0-preview.24
Un processus graphique lancé depuis le Finder ne porte pas LANG/LC_*, donc .NET démarre invariant même sur une machine pt-BR. Les shells résolvent la vérité de la locale de la plateforme avant la première frame : NSLocale.preferredLanguages[0] comme culture d'interface et currentLocale comme culture de format chez Apple (la même séparation que .NET modélise entre CurrentUICulture et CurrentCulture), Locale.getDefault() alimentant les deux sur Android, et PhotonCultureController copie la paire sur les statiques du processus, en repeignant lors d'un Apply ultérieur exactement comme le contrôleur de thème repeint lors d'un changement de mode. Comme Photon exécute du vrai .NET sans transpilation, ResourceManager et les assemblys satellites y fonctionnent déjà : une application native se localise aujourd'hui avec du resx ordinaire.
Une page parle resx sur le web, de bout en bout
Depuis 0.2.0-preview.26
Le M0 du Track L : une application écrit la localisation exactement comme le fait toute application .NET : un Resources/Strings.resx plus Strings.pt-BR.resx, les accesseurs Designer ordinaires, string.Format(Strings.Greeting, name), et la page répond dans la culture de la requête sur LES DEUX moitiés de la cible web.
GET /page + Accept-Language
choisit le catalogue (exact, parent, neutre)
shell + window.__EQ_CULTURE__
le boot installe la culture
hydrate avec les MÊMES chaînes
Navigateur
Serveur
Runtime
Les pièces, reflétant la forme du pont de thème trait pour trait :
•
eqc réécrit les accesseurs, il ne les inline jamais. Strings.Hero_Title compile en $eq.str("Strings", "Hero.Title"), parce qu'inliner cuirait la culture de la machine de build dans le bundle. La détection se fait par FORME (les propriétés statiques ResourceManager + Culture du Designer), donc n'importe quel resx n'importe où dans le projet fonctionne, quel que soit le nom que lui a donné qui développe.
•
Le build émet des catalogues à partir des clés que l'application utilise vraiment. wwwroot/_equantic/strings/neutral.json plus un {culture}.json par variante écrite, chacun APLATI contre la chaîne de repli de .NET au moment de l'émission : le serveur choisit un fichier, il ne fusionne jamais.
•
Le serveur inline la réponse. UseRequestLocalization (l'APPLICATION câble la négociation, le SDK ne lit que ce que le middleware a posé) conduit <html lang> et un emplacement de shell window.__EQ_CULTURE__ = { name, formatName, format, strings } ; le boot l'installe AVANT l'hydratation, donc le client résout exactement les chaînes que le serveur a rendues. format est le NumberFormatInfo et le DateTimeFormatInfo propres à la culture de format, écrits depuis .NET sur chaque page, que l'application ait ou non une chaîne à traduire : le navigateur écrit chaque nombre et chaque date, et nomme chaque mois d'un calendrier, à partir de cela (Depuis 0.2.0-preview.61).
•
string.Format sur un accesseur est validé au build. EQ2100 rejette les arguments qui ne sont pas des chaînes, les spécificateurs de format et les écarts d'arité face au resx neutre, le sous-ensemble M0 du D7 du plan.
Changer de langue re-rend, cela ne recharge jamais
Depuis 0.2.0-preview.27
ICultureController est la main de l'application sur la LANGUE, dans la forme d'IThemeController, résolu par nom d'interface, si bien qu'un composant qui offre un sélecteur n'apprend jamais quelle cible a répondu. Il parle des noms BCP-47, jamais CultureInfo : le contrat traverse vers un navigateur où ce type n'existe pas, et un nom est la seule monnaie que .NET, le web et les deux plateformes mobiles partagent.
•
Web : setCulture télécharge le catalogue de la culture seulement quand il n'est pas déjà en mémoire, en choisissant le fichier comme le SERVEUR le choisit (exact → parents → neutre ; la chaîne a été aplatie au build, la recherche reste donc plate), et les données de la culture de format depuis /_equantic/culture/{name}.json (Depuis 0.2.0-preview.61), les échange, et re-rend par le même ordonnanceur qu'utilise SetState. L'état survit ; seul le texte qui a changé est touché. Le changement survit aussi à la page : le contrôleur écrit le cookie de culture propre à ASP.NET, dans le format propre à ASP.NET, donc la requête suivante arrive déjà traduite.
•
Natif : PhotonCultureController réalise la même interface : les statiques du processus changent et la fenêtre se repeint.
•
CultureSwitcher est la surface intégrée : un contrôle segmenté pour deux ou trois langues, un menu au-delà, qui allume l'option active par nom exact puis par la part de langue (une page servie en pt-BR doit allumer une entrée pt).
1
2
3
4
5
new CultureSwitcher([
new("en", "English"),
new("pt-BR", "Português"),
new("es", "Español"),
])
Un spécificateur de format survit au voyage
Depuis 0.2.0-preview.27
{0:C2} écrit une fois en C# imprime R$ 1.234,50 sur une page pt-BR et 1234,50 € sur une page es, le SSR et le client s'accordant caractère pour caractère, et un changement de culture reformate en direct avec les chaînes. Le sous-ensemble D7 : N/F/P/C/D/E avec précision, les lettres standard de date et d'heure, les gabarits personnalisés de chiffres et de date, et le {0} nu (qui n'est PAS invariant en .NET, et qui ne l'est plus dans le navigateur non plus).
La preuve est la méthode : la fixture épinglée est générée depuis du vrai .NET (value.ToString(spec, culture), sept cultures choisies pour se contredire) et le runtime transpilé doit la reproduire octet pour octet, une espace insécable comprise. Cette direction a attrapé chaque divergence au build. Intl et .NET lisent des builds d'ICU différents et disposent un nombre selon des tables différentes : Intl écrivait les chiffres propres à ar-EG là où .NET écrit les ASCII, une espace insécable là où le motif monétaire de .NET en a une normale, et deux chiffres pour N là où .NET en lit trois sur ICU. Donc le navigateur dessine chaque nombre et chaque date depuis les NumberFormatInfo et DateTimeFormatInfo propres à la culture, que le serveur envoie avec la page, et Intl ne dispose qu'une culture vers laquelle on a basculé sans serveur à interroger (Depuis 0.2.0-preview.61).
Ce qui ne peut pas s'accorder est refusé au build, jamais approché : EQ2100 rejette l'alignement et les spécificateurs hors sous-ensemble dans un gabarit resx ; EQ2101 tient les placeholders de chaque traduction face au resx neutre, donc une chaîne pt-BR qui demande {2} là où le neutre a {0}/{1} fait échouer le build au lieu de lever pour les lectrices brésiliennes seules.
La portée, dite simplement : la promesse d'accord exact pour les CHAÎNES couvre les cultures que l'application livre, et une culture négociée à l'exécution sans catalogue écrit dégrade vers les chaînes neutres. Ses FORMATS sont ceux de la culture de la requête, quels que soient les catalogues existants, écrits depuis les données que le serveur envoie, déterministes et jamais dépendants du navigateur ; une page sans culture installée formate dans la culture invariante (Depuis 0.2.0-preview.61).
Deux cultures dans une fenêtre
Depuis 0.2.0-preview.29
La moitié web de cette histoire est une requête : une culture entre, le SSR et l'hydratation s'accordent. Une fenêtre n'a pas de requête, donc un changement là-bas a trois articulations que le web n'a jamais : la plateforme décide de la locale initiale, quelqu'un la copie sur les statiques de .NET, et l'arbre doit se RECONSTRUIRE pour que le changement atteigne les pixels.
Un composant ne dit rien de tout cela. La section Language de l'exemple Studio est un CultureSwitcher ordinaire, qui résout ICultureController depuis le contexte et ne nomme aucune plateforme... la même classe que rend le navigateur. Appliquer une culture écrit les deux statiques (la paire du D13 : les ressources et les formats sont des décisions séparées) et repeint la fenêtre, donc le changement atterrit à la frame suivante avec chaque bit d'état de page intact.
Le chemin de capture d'écran le rend des deux façons, et c'est ce qui a clos le jalon : sans argument la fenêtre montre la paire de la MACHINE (une interface en anglais avec des formats portugais, sur un Mac réglé ainsi), et avec --culture pt-BR elle montre Marcado, Ativado, Planilha à côté de R$ 1.234,50.
Écrire l'exemple a trouvé le bug que les tests ne pouvaient pas : l'hôte n'enregistrait que le contrôleur concret, donc un composant demandant l'INTERFACE résolvait null et le sélecteur ne changeait rien. Deux enregistrements, une instance. Une capacité que personne ne résout dans une vraie fenêtre est une capacité qui ne fonctionne pas.
Une nouvelle application naît localisée
Depuis 0.2.0-preview.28
dotnet new equantic-app génère maintenant ce que cette page prêche : un Resources/Strings.resx avec son satellite pt-BR et l'accesseur Designer ordinaire, un CultureSwitcher sur la page d'accueil, et toute l'histoire en une ligne, string.Format(Strings.CountedTimes, _count). Le premier projet de qui développe répond en deux langues avant d'avoir écrit du code, et c'est le moment le moins cher pour apprendre que les chaînes d'interface ne se codent jamais en dur. Voir Premiers pas pour le scaffold complet.
On dit à un crawler quelle URL est quelle langue
Depuis 0.2.0-preview.31
Traduire un site et ne jamais déclarer le groupe de traduction, c'est ainsi qu'un site localisé se classe en silence contre lui-même. Une seule politique pour toute l'application, options.UseAlternateLinks(AlternateUrls.PathPrefix(), "en", "pt-BR", "es"), met tout l'ensemble rel="alternate" hreflang plus x-default dans le head de chaque page, avec les règles de réciprocité et d'URL absolue appliquées plutôt que laissées à l'application. Voir Intégration serveur.
La langue voyage dans l'URL
Depuis 0.2.0-preview.36
Un site public veut chaque traduction à sa propre adresse (/pt-BR/pricing) parce que c'est la seule forme qu'un crawler indexe comme une traduction et qu'un lecteur peut partager. Une déclaration le fait :
1
2
3
4
5
6
builder.Services.AddUI(options => options
.ScanAssembly(typeof(Program).Assembly)
.UseCultureRoutes("en", "pt-BR", "es")); // la première est servie sans préfixe
…
app.UseRequestLocalization(); // le middleware propre à ASP.NET, déjà configuré
app.MapUI();
Rien là-dedans n'est de la machinerie propre au SDK. UseCultureRoutes remplit les RequestLocalizationOptions de la plateforme depuis cette liste (culture par défaut, cultures prises en charge, et le RouteDataRequestCultureProvider d'ASP.NET placé en premier), et enregistre une contrainte de route culture, si bien que chaque page (une [Page] ou un MapPage<T>) est aussi mappée à /{culture:culture}/…. À partir de là :
•
/pt-BR/pricing se rend en pt-BR, et le segment bat le cookie et Accept-Language : une URL qui nomme une langue est une promesse sur ce que dit la page.
•
Chaque href interne porte la langue : l'auteur écrit /about une fois et le serveur comme le navigateur émettent /pt-BR/about pendant que le lecteur est en portugais. Un Link, un TextRun qui lie et un lien Markdown de même (un run et un lien Markdown depuis la 0.2.0-preview.60 : ils gardaient la destination telle qu'écrite), et chacun des internes est préchauffé au survol. Les URL absolues, celles relatives au protocole (//cdn…), les ancres et mailto: sont laissées tranquilles.
•
/fr/pricing est un 404, pas de l'anglais sous une adresse française : la contrainte admet exactement les préfixes que vous avez nommés.
•
Un /pricing nu demandé en portugais (le cookie qu'écrit CultureSwitcher, ou Accept-Language) est redirigé (302) vers /pt-BR/pricing, donc ce qui est vu, partagé et mis en favori nomme toujours sa langue. La culture par défaut garde ses URL nues.
•
Changer de langue est une navigation : CultureSwitcher va à la même page à son autre adresse, par le routeur, sans rechargement.
•
hreflang et x-default viennent de la même carte sans seconde déclaration, et un canonical qu'une page a écrit à la main est localisé en sortie, donc il ne pointe jamais un crawler vers la mauvaise traduction.
Une application qui configure le middleware avec la surcharge lambda (app.UseRequestLocalization(o => …)) construit ses options en ligne et ne voit rien de la DI, donnez-lui donc la même liste là avec o.UseCultureRoutes("en", "pt-BR", "es").
Encore de la conception (Track L, docs/I18N-PLAN.md)
Les satellites sous NativeAOT ne sont plus une question : un harnais exécutable (scripts/verify-aot-satellites.sh) publie un binaire natif contre la bibliothèque de composants et refuse de passer à moins que en, pt-BR et es répondent tous avec la TRADUCTION, parce qu'un satellite perdu ne plante pas, il répond silencieusement en anglais, donc la vérification compare aux traductions attendues.
Encore devant nous : la découpe des catalogues par page, les formes plurielles au-delà de deux, et IHandleMetadata qui se rejoue lors d'un changement de culture. Le RTL et la couverture de police par écriture sont explicitement d'autres voies.