eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsArquitectura
Localización
Edit this page
13 min read
El multi-idioma en eQuantic.UI sigue una sola ley: quien programa localiza con lo que .NET mismo ofrece: .resx, el accesor fuertemente tipado, CultureInfo. Nunca ve un catálogo de JavaScript ni un DSL del framework. El diseño completo (Track L: cómo eqc puentea ResourceManager hacia el navegador, los catálogos por cultura, el subconjunto de formato, las vallas honestas) vive en el repositorio en docs/I18N-PLAN.md. Esta página documenta lo que está entregado hoy y lo que sigue siendo diseño.
Entregado
Las cadenas propias del SDK viven detrás de una sola costura
Desde 0.2.0-preview.24
Los componentes nunca escriben a mano una cadena de cara al usuario. Cada etiqueta incorporada (los anuncios de alternancia que oye un lector de pantalla ("Checked", "On"), los recursos para descartar, el placeholder de búsqueda, la barra de buscar del editor de código) lee SdkStrings, una clase estática de propiedades (nunca constantes: una constante cruza ensamblados por VALOR, y los sitios de llamada inlineados seguirían anunciando el idioma de la máquina de build después de que la costura aprenda culturas). Esa costura está respaldada por resx hoy: cada propiedad lee SdkResources, un accesor Designer corriente, y ningún componente cambió cuando lo hizo. En la web eqc reescribe el accesor como una búsqueda en el catálogo de cultura que el shell inlinea, así que la misma propiedad responde desde un ensamblado satélite en el servidor y desde JSON en el navegador.
Una aplicación con cero resx propios ya está localizada por ello: las claves del SDK se suman al catálogo de cada cultura que la aplicación emite, así que un lector de pantalla oye "Marcado" en una página pt-BR que nadie tradujo.
Qué idiomas se entregan, y cómo entregar menos. Las cadenas del SDK están traducidas a pt-BR y es hoy, y viajan DENTRO del paquete como ensamblados satélite (lib/net10.0/<culture>/eQuantic.UI.Components.resources.dll, unos 8 KB cada uno): un paquete que lleva sus idiomas, como entregan los suyos Roslyn y la plataforma de pruebas, en lugar de un paquete por idioma. Una aplicación que solo quiere algunos de ellos no necesita un paquete distinto, porque .NET ya es dueño de esa decisión:
1
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
Medido en un consumidor del paquete publicado: sin él la salida de la aplicación lleva es/ y pt-BR/; con él, ninguno. El único sitio donde no se aplica es una referencia de PROYECTO: los ejemplos de este repositorio conservan los satélites de cualquier forma, porque la propiedad filtra lo que llega a través de un paquete.
Las ventanas de Photon adoptan el locale de la máquina al arrancar
Desde 0.2.0-preview.24
Un proceso de GUI lanzado desde Finder no lleva LANG/LC_*, así que .NET arranca invariante incluso en una máquina pt-BR. Los shells resuelven la verdad del locale de la plataforma antes del primer frame: NSLocale.preferredLanguages[0] como cultura de interfaz y currentLocale como cultura de formato en Apple (la misma división que .NET modela como CurrentUICulture frente a CurrentCulture), Locale.getDefault() alimentando las dos en Android, y PhotonCultureController copia el par en las estáticas del proceso, repintando en un Apply posterior exactamente como el controlador de tema repinta al cambiar de modo. Como Photon corre .NET de verdad sin transpilación, ResourceManager y los ensamblados satélite ya funcionan ahí: una aplicación nativa se localiza hoy con resx corriente.
Una página habla resx en la web, de punta a punta
Desde 0.2.0-preview.26
El M0 del Track L: una aplicación escribe la localización exactamente como lo hace cualquier aplicación .NET: un Resources/Strings.resx más Strings.pt-BR.resx, los accesores Designer corrientes, string.Format(Strings.Greeting, name), y la página responde en la cultura de la petición en LAS DOS mitades del destino web.
GET /page + Accept-Language
elige el catálogo (exacto, padre, neutro)
shell + window.__EQ_CULTURE__
el boot instala la cultura
hidrata con las MISMAS cadenas
Navegador
Servidor
Runtime
Las piezas, reflejando la forma del puente de tema punto por punto:
•
eqc reescribe los accesores, nunca los inlinea. Strings.Hero_Title compila a $eq.str("Strings", "Hero.Title"), porque inlinear hornearía la cultura de la máquina de build dentro del bundle. La detección es por FORMA (las propiedades estáticas ResourceManager + Culture del Designer), así que cualquier resx en cualquier sitio del proyecto funciona, como sea que lo haya llamado quien programa.
•
El build emite catálogos a partir de las claves que la aplicación usa de verdad. wwwroot/_equantic/strings/neutral.json más un {culture}.json por variante escrita, cada uno APLANADO contra la cadena de fallback de .NET en el momento de emitir: el servidor elige un archivo, nunca fusiona.
•
El servidor inlinea la respuesta. UseRequestLocalization (la APLICACIÓN conecta la negociación, el SDK solo lee lo que el middleware fijó) conduce <html lang> y una ranura del shell window.__EQ_CULTURE__ = { name, formatName, format, strings }; el boot la instala ANTES de la hidratación, así que el cliente resuelve exactamente las cadenas que el servidor renderizó. format son el NumberFormatInfo y el DateTimeFormatInfo propios de la cultura de formato, escritos desde .NET en cada página, tenga o no la aplicación una cadena que traducir: el navegador escribe cada número y cada fecha, y nombra cada mes de un calendario, a partir de ello (Desde 0.2.0-preview.61).
•
string.Format sobre un accesor se valida en el build. EQ2100 rechaza argumentos que no son cadenas, especificadores de formato y desvíos de aridad contra el resx neutro, el subconjunto M0 del D7 del plan.
Cambiar de idioma re-renderiza, nunca recarga
Desde 0.2.0-preview.27
ICultureController es la mano de la aplicación sobre el IDIOMA, con la forma de IThemeController, resuelto por nombre de interfaz, así que un componente que ofrece un switcher nunca llega a saber qué destino respondió. Habla nombres BCP-47, nunca CultureInfo: el contrato cruza a un navegador donde ese tipo no existe, y un nombre es la única moneda que comparten .NET, la web y las dos plataformas móviles.
•
Web: setCulture descarga el catálogo de la cultura solo cuando no está ya en memoria, eligiendo el archivo como lo elige el SERVIDOR (exacto → padres → neutro; la cadena se aplanó en el build, así que la búsqueda se queda plana), y los datos de la cultura de formato desde /_equantic/culture/{name}.json (Desde 0.2.0-preview.61), los cambia, y re-renderiza por el mismo planificador que usa SetState. El estado sobrevive; solo se toca el texto que cambió. El cambio además sobrevive a la página: el controlador escribe la propia cookie de cultura de ASP.NET, en el formato propio de ASP.NET, así que la siguiente petición llega ya traducida.
•
Nativo: PhotonCultureController realiza la misma interfaz: las estáticas del proceso cambian y la ventana se repinta.
•
CultureSwitcher es la superficie incorporada: un control segmentado para dos o tres idiomas, un menú más allá de eso, encendiendo la opción activa por nombre exacto y luego por la parte de idioma (una página servida como pt-BR debe encender una entrada pt).
1
2
3
4
5
new CultureSwitcher([
new("en", "English"),
new("pt-BR", "Português"),
new("es", "Español"),
])
Un especificador de formato sobrevive al viaje
Desde 0.2.0-preview.27
{0:C2} escrito una vez en C# imprime R$ 1.234,50 en una página pt-BR y 1234,50 € en una es, con el SSR y el cliente coincidiendo carácter por carácter, y un cambio de cultura re-formatea en vivo junto con las cadenas. El subconjunto D7: N/F/P/C/D/E con precisión, las letras estándar de fecha y hora, las plantillas personalizadas de dígitos y de fecha, y el {0} pelado (que NO es invariante en .NET, y ahora tampoco lo es en el navegador).
La prueba es el método: el fixture fijado se genera desde .NET real (value.ToString(spec, culture), siete culturas elegidas para discrepar) y el runtime transpilado debe reproducirlo byte por byte, un espacio duro incluido. Esa dirección atrapó cada divergencia en tiempo de build. Intl y .NET leen builds distintos de ICU y disponen un número por tablas distintas: Intl escribía los dígitos propios de ar-EG donde .NET escribe los ASCII, un espacio duro donde el patrón de moneda de .NET tiene uno normal, y dos dígitos para N donde .NET lee tres en ICU. Así que el navegador dibuja cada número y cada fecha desde el NumberFormatInfo y el DateTimeFormatInfo propios de la cultura, que el servidor envía con la página, e Intl solo dispone una cultura a la que se cambió sin un servidor al que preguntar (Desde 0.2.0-preview.61).
Lo que no puede coincidir se rechaza en el build, nunca se aproxima: EQ2100 rechaza la alineación y los especificadores fuera del subconjunto en una plantilla resx; EQ2101 sostiene los placeholders de cada traducción contra el resx neutro, así que una cadena pt-BR que pide {2} donde el neutro tiene {0}/{1} falla el build en lugar de lanzar solo para quien lee en Brasil.
El alcance, dicho con claridad: la promesa de coincidencia exacta para las CADENAS cubre las culturas que la aplicación entrega, y una cultura negociada en tiempo de ejecución sin catálogo escrito degrada a las cadenas neutras. Sus FORMATOS son los de la cultura de la petición, existan los catálogos que existan, escritos desde los datos que envía el servidor, deterministas y nunca dependientes del navegador; una página sin cultura instalada formatea en la cultura invariante (Desde 0.2.0-preview.61).
Dos culturas en una ventana
Desde 0.2.0-preview.29
La mitad web de esta historia es una petición: una cultura entra, SSR e hidratación coinciden. Una ventana no tiene petición, así que un cambio ahí tiene tres articulaciones que la web nunca tiene: la plataforma decide el locale inicial, alguien lo copia a las estáticas de .NET, y el árbol tiene que RECONSTRUIRSE para que el cambio llegue a los píxeles.
Un componente no dice nada de eso. La sección Language del ejemplo Studio es un CultureSwitcher corriente, que resuelve ICultureController del contexto y no nombra ninguna plataforma... la misma clase que renderiza el navegador. Aplicar una cultura escribe las dos estáticas (el par del D13: los recursos y los formatos son decisiones separadas) y repinta la ventana, así que el cambio aterriza en el siguiente frame con cada bit de estado de la página intacto.
El camino de captura de pantalla lo renderiza de las dos formas, que es lo que cerró el hito: sin argumento la ventana muestra el par de la MÁQUINA (una interfaz en inglés con formatos portugueses, en un Mac configurado así), y con --culture pt-BR muestra Marcado, Ativado, Planilha al lado de R$ 1.234,50.
Escribir el ejemplo encontró el bug que los tests no podían: el host registraba solo el controlador concreto, así que un componente que pedía la INTERFAZ resolvía null y el switcher no cambiaba nada. Dos registros, una instancia. Una capacidad que nadie resuelve en una ventana de verdad es una capacidad que no funciona.
Una aplicación nueva nace localizada
Desde 0.2.0-preview.28
dotnet new equantic-app ahora genera lo que esta página predica: un Resources/Strings.resx con su satélite pt-BR y el accesor Designer corriente, un CultureSwitcher en la página de inicio, y toda la historia en una línea, string.Format(Strings.CountedTimes, _count). El primer proyecto de quien programa responde en dos idiomas antes de haber escrito código, que es el momento más barato para aprender que las cadenas de interfaz nunca se escriben a mano. Vea Primeros pasos para el scaffold completo.
A un crawler se le dice qué URL es qué idioma
Desde 0.2.0-preview.31
Traducir un sitio y no declarar nunca el grupo de traducción es como un sitio localizado compite en silencio contra sí mismo. Una sola política para toda la aplicación, options.UseAlternateLinks(AlternateUrls.PathPrefix(), "en", "pt-BR", "es"), pone el conjunto completo de rel="alternate" hreflang más x-default en el head de cada página, con las reglas de reciprocidad y de URL absoluta aplicadas en lugar de dejadas a la aplicación. Vea Integración con el servidor.
El idioma viaja en la URL
Desde 0.2.0-preview.36
Un sitio público quiere cada traducción en su propia dirección (/pt-BR/pricing) porque es la única forma que un crawler indexa como traducción y que quien lee puede compartir. Una declaración lo hace:
1
2
3
4
5
6
builder.Services.AddUI(options => options
.ScanAssembly(typeof(Program).Assembly)
.UseCultureRoutes("en", "pt-BR", "es")); // la primera se sirve sin prefijo
…
app.UseRequestLocalization(); // el middleware propio de ASP.NET, ya configurado
app.MapUI();
Nada de eso es maquinaria propia del SDK. UseCultureRoutes llena las RequestLocalizationOptions de la plataforma a partir de esa lista (cultura por defecto, culturas soportadas, y el RouteDataRequestCultureProvider de ASP.NET puesto primero), y registra una restricción de ruta culture, así que cada página (una [Page] o un MapPage<T>) se mapea también en /{culture:culture}/…. A partir de ahí:
•
/pt-BR/pricing se renderiza en pt-BR, y el segmento le gana a la cookie y a Accept-Language: una URL que nombra un idioma es una promesa sobre lo que dice la página.
•
Cada href interno de la aplicación lleva el idioma: quien escribe pone /about una vez y el servidor y el navegador emiten los dos /pt-BR/about mientras quien lee está en portugués. Un Link, un TextRun que enlaza y un enlace de Markdown igual (un run y un enlace de Markdown desde 0.2.0-preview.60: conservaban el destino tal como se escribió), y cada uno interno se precalienta al pasar el ratón. Las URLs absolutas, las relativas al protocolo (//cdn…), los anclajes y mailto: se dejan en paz.
•
/fr/pricing es un 404, no inglés bajo una dirección francesa: la restricción admite exactamente los prefijos que nombraste.
•
Un /pricing pelado pedido en portugués (la cookie que escribe CultureSwitcher, o Accept-Language) se redirige (302) a /pt-BR/pricing, así que lo que se ve, se comparte y se marca siempre nombra su idioma. La cultura por defecto conserva sus URLs peladas.
•
Cambiar de idioma es una navegación: CultureSwitcher va a la misma página en su otra dirección, por el router, sin recargar.
•
hreflang y x-default vienen del mismo mapa sin una segunda declaración, y un canonical que una página escribió a mano se localiza a la salida, así que nunca apunta a un crawler a la traducción equivocada.
Una aplicación que configura el middleware con la sobrecarga de lambda (app.UseRequestLocalization(o => …)) construye sus opciones inline y no ve nada de la DI, así que pásale la misma lista ahí con o.UseCultureRoutes("en", "pt-BR", "es").
Todavía diseño (Track L, docs/I18N-PLAN.md)
Los satélites bajo NativeAOT ya no son una pregunta: un arnés ejecutable (scripts/verify-aot-satellites.sh) publica un binario nativo contra la biblioteca de componentes y se niega a pasar a menos que en, pt-BR y es respondan todos con la TRADUCCIÓN, porque un satélite perdido no revienta, responde en inglés en silencio, así que la comprobación compara contra las traducciones esperadas.
Todavía por delante: dividir catálogos por página, formas plurales más allá de dos, y que IHandleMetadata se vuelva a ejecutar en un cambio de cultura. El RTL y la cobertura de tipografía por escritura son explícitamente otras vías.