-
Notifications
You must be signed in to change notification settings - Fork 1
Localization
🌐 This page in: English · Português
Multi-language in eQuantic.UI follows one law: the developer localizes with what .NET itself
offers — .resx, the strongly-typed accessor, CultureInfo — and never sees a JavaScript
catalog or a framework DSL. The full design (Track L: how eqc bridges ResourceManager into
the browser, per-culture catalogs, the formatting subset, the honest fences) lives in the repo at
docs/I18N-PLAN.md. This page documents what is shipped today and what is still design.
Since 0.2.0-preview.24
Components never hardcode a user-facing string. Every built-in label — the toggle announcements a
screen reader hears ("Checked", "On"), the dismiss affordances, the search placeholder, the code
editor's find bar — reads SdkStrings, a static class of properties (never consts: a const
crosses assemblies by VALUE, and inlined call sites would keep announcing the build machine's
language after the seam learns cultures). The transpiled twins confirm the choke point survives on
the web too: eqc emits static getters and the components reference them. When Track L lands, the
property bodies swap to the resx-backed accessor and no component changes.
Since 0.2.0-preview.24
A GUI process launched from Finder carries no LANG/LC_*, so .NET starts invariant even on a
pt-BR machine. The shells resolve the platform's locale truth before the first frame —
NSLocale.preferredLanguages[0] as the UI culture and currentLocale as the format culture on
Apple (the same split .NET models as CurrentUICulture vs CurrentCulture), Locale.getDefault()
feeding both on Android — and PhotonCultureController copies the pair onto the process statics,
repainting on a later Apply exactly as the theme controller repaints on a mode flip. Because
Photon runs real .NET with no transpilation, ResourceManager and satellite assemblies already
work there: a native app localizes today with ordinary resx.
Since 0.2.0-preview.26
Track L's M0: an app writes localization exactly as any .NET app does — a Resources/Strings.resx
plus Strings.pt-BR.resx, the ordinary Designer accessors, string.Format(Strings.Greeting, name)
— and the page answers in the request's culture on BOTH halves of the web target.
sequenceDiagram
participant B as Browser
participant S as Server
participant R as Runtime
B->>S: GET /page + Accept-Language
S->>S: pick catalog (exact, parent, neutral)
S-->>B: shell + window.__EQ_CULTURE__
B->>R: boot installs culture
R->>R: hydrate with the SAME strings
The pieces, mirroring the theme bridge shape for shape:
-
eqcrewrites accessors, never inlines them.Strings.Hero_Titlecompiles to$eq.str("Strings", "Hero.Title")— inlining would bake the build machine's culture into the bundle. Detection is by SHAPE (the Designer's staticResourceManager+Cultureproperties), so any resx anywhere in the project works, whatever the developer named it. -
The build emits catalogs from the keys the app actually uses.
wwwroot/_equantic/strings/neutral.jsonplus one{culture}.jsonper authored variant, each FLATTENED against the .NET fallback chain at emit time — the server picks a file, it never merges. -
The server inlines the answer.
UseRequestLocalization(the APP wires negotiation, the SDK only reads what the middleware set) drives<html lang>and awindow.__EQ_CULTURE__ = { name, formatName, strings }shell slot; boot installs it BEFORE hydration, so the client resolves exactly the strings the server rendered. -
string.Formatover an accessor is validated at build. EQ2100 rejects non-string arguments, format specifiers and arity drift against the neutral resx — the M0 subset of the plan's D7.
Since 0.2.0-preview.27
ICultureController is the app's hand on the LANGUAGE — the IThemeController shape, resolved
by interface name, so a component offering a switcher never learns which target answered. It
speaks BCP-47 names, never CultureInfo: the contract crosses to a browser where no such
type exists, and a name is the one currency .NET, the web and both mobile platforms share.
-
Web:
setCulturefetches the culture's catalog only when it is not already in memory — picking the file the way the SERVER picks it (exact → parents → neutral; the chain was flattened at build, so the lookup stays flat) — swaps it, and re-renders through the same schedulerSetStateuses. State survives; only changed text is touched. The switch also outlives the page: the controller writes ASP.NET's own culture cookie, in ASP.NET's own format, so the next request arrives already translated. -
Native:
PhotonCultureControllerrealizes the same interface — the process statics change and the window repaints. -
CultureSwitcheris the built-in surface: a segmented control for two or three languages, a menu beyond that, lighting the active option by exact name then by language part (a page served aspt-BRmust light aptentry).
new CultureSwitcher([
new("en", "English"),
new("pt-BR", "Português"),
new("es", "Español"),
])Since 0.2.0-preview.27
{0:C2} written once in C# prints R$ 1.234,50 on a pt-BR page and 1234,50 € on an es one
— SSR and client agreeing character for character, and a culture switch re-formats live with the
strings. The D7 subset: N/F/P/C/D/E with precision, the standard date/time letters,
custom digit and date pictures, and the bare {0} (which is NOT invariant in .NET, and now is
not in the browser either).
The proof is the method: the pinned fixture is generated from real .NET
(value.ToString(spec, culture), three cultures chosen to disagree) and the transpiled runtime
must reproduce it exactly. That direction caught every divergence at build time — Intl rounds
midpoints away from zero where .NET banks them, so the formatter pre-rounds with the banker's
helper; Intl's presets drop en-US's short date to a two-digit year, so each culture's own
patterns travel in its catalog as $-facts beside the strings, with the ISO currency code
(Intl demands a code, and no browser API derives one from a locale).
What cannot agree is refused at build, never approximated: EQ2100 rejects alignment and
out-of-subset specifiers in a resx template; EQ2101 holds every translation's placeholders
against the neutral resx, so a pt-BR string asking for {2} where neutral has {0}/{1} fails
the build instead of throwing for Brazilian readers only.
Scope, stated plainly: the exact-agreement promise covers the cultures the app SHIPS. A culture negotiated at runtime with no authored catalog degrades to neutral strings and neutral (invariant) formats — deterministic, never browser-dependent.
Satellites under NativeAOT are no longer a question: an executable harness
(scripts/verify-aot-satellites.sh) publishes a native binary against the component library and
refuses to pass unless en/pt-BR/es all answer with the TRANSLATION — a dropped satellite does not
crash, it silently answers English, so the check compares against expected translations.
Still ahead: per-page catalog splitting, plural forms beyond two, and IHandleMetadata re-running
on a culture switch. RTL and per-script font coverage are explicitly other tracks.
🌐 English · Português
🏁 Start here
📱 Write-once
- Write-Once Components
- Declarative Surface
- Photon Engine
- Design System
- Capabilities
- Storage
- Forms
- Code Editor
- Markdown
- Mermaid
- Email Rendering
🏗️ Architecture
⚙️ Compilation
- Compiler
- Compile-Time Evaluation
- Supported C# Features
- External Type Resolution
- Build Flow
- Diagnostics
⚡ Runtime
🔌 Server
🎨 Ecosystem
🚀 Development