I18n turns stable machine semantics into text a current user can read. It owns translation reads and writes, language-catalog reads, the runtime localizer, and cache invalidation. Request-language resolution lives in the API host, while MasterData Infrastructure physically defines the language and translation records.
1. Implemented capabilities
- Request language resolution through
Accept-Language → bitzorcas.lang cookie → ?lang= → setting → zh-CN; - synchronization of
ILanguageContext,CurrentCulture,CurrentUICulture,Content-Language, and the globalL()AsyncLocal; Platform,Tenant, andTenantOfficetranslation scopes;- database merge in which Platform is applied first, Tenant second, and TenantOffice last;
- a unique translation key over Key, LanguageCode, ScopeType, TenantId, and OfficeId;
SaveTranslationupsert, local tag eviction, cross-instance resource-sync notification, and frontend notification;- database, module-JSON, fallback-language, and final-key behavior for single-key
GetString; - translation dictionaries partitioned by language/tenant/office with a 30-minute TTL;
- ORM-neutral
IEntitySet<T>read models with a basic SqlSugar/EF Core parity scenario; - fail-closed persistence defaults for four ports in the production composition root.
There are no language create/update/disable/set-default endpoints, translation delete/import/review/publish endpoints, parameter-format contract, ICU MessageFormat support, plural or gender rules, revision history, optimistic concurrency, audit/outbox, resource ETags, JSON hot reload, enabled-language enforcement, or complete HTTP and multi-instance tests.
2. Runtime structure
I18n Infrastructure directly references MasterData Infrastructure to reuse SysLanguageCatalogRecord and SysTranslationCatalogRecord. That physical dependency disagrees with the I18nModule governance declaration, which lists only Authorization. This owner and governance mismatch remains architectural debt.
3. Four HTTP routes
| Method and route | Resource / action | Current behavior |
|---|---|---|
GET /api/i18n/languages | i18n/languages / View | Reads enabled or all languages for the current tenant |
GET /api/i18n/translations | i18n/translations / View | Merges database translations for language/current tenant/office and can push down KeyPrefix |
GET /api/i18n/resources | i18n/translations / View | Intended frontend bundle; currently empty because of the no-key batch path |
POST /api/i18n/translations | i18n/translations / Update | Saves an Active translation and performs three notification steps |
Source generation produces all four endpoints. The permission catalog declares three stable codes (i18n.languages.read, i18n.translations.read, i18n.translations.manage), while requests expose generic Resource/Action data. Real HTTP authorization tests must prove the final mapping from actions to i18n.* codes.
4. Request language is not catalog authorization
The middleware validates only the syntactic shape of a language tag. It does not query the current tenant’s SysLanguage rows, disabled flag, or default language. zz-ZZ can pass the regular expression; when .NET rejects it, Culture assignment is skipped but ILanguageContext and the response header retain the value.
Accept-Language processing takes only the first list item and removes its q parameter. It neither ranks q values nor applies region fallback. Header wins over cookie and query, so ?lang= cannot override an automatically supplied browser header under the current contract.
5. Translation precedence
The store orders by numeric ScopeType and writes into one dictionary, so later scopes overwrite earlier scopes. It does not reject malformed combinations. A Tenant row with a nonzero OfficeId still applies as a Tenant override, for example. Database and write-side validation must enforce the invariant together.
6. Save an office translation
// A real tenant and office context are required; the handler now fail-closes on missing context.var command = new SaveTranslation.Command( Key: "Billing.Invoice.Actions.Submit", LanguageCode: "en-US", Scope: TranslationScope.TenantOffice, Value: "Submit invoice", Context: "Bulk action on the invoice list");
var result = await sender.Send(command, cancellationToken);
// Success means every awaited step returned successfully. It does not mean// the database write and notifications share one transaction or outbox.if (result.IsFailure) return result.Error;A normal User caller cannot write Platform scope. Platform-scope writes now require the caller to hold the RootCrossTenant permission and pass ActorKey.TryCreatePlatformOperator identity verification; once both hold, the handler builds a RootCrossTenantOperation capability ticket (operation code i18n.platform-translation.manage) passed down to the repository for re-validation. Tenant/Office scope with a missing trusted context is rejected by the handler, no longer silently coerced to "0". See translation scopes and persistence for the full mechanism.
7. Single-key and batch divergence
GetStringsAsync does not reuse this chain. It loads only the requested-language database dictionary and, for each supplied key, returns a hit or the final key segment. It uses neither JSON nor fallback language. With no input keys, it iterates zero times and discards the full dictionary loaded from the store.
8. Language-catalog boundary
GetLanguages queries only SysLanguage rows whose TenantId exactly equals the current tenant. Platform or blank-tenant seed rows are not merged as a fallback. MasterData’s 110-sys_language.csv contains zh-CN and en-US, but there is no evidence that it clones those rows for each newly provisioned tenant.
The catalog declares only a language read use case (GetLanguages); there is no language management handler (create/update/disable/set-default). ILanguageRepository is an empty marker. Methods present on DefaultLanguageRepository are not members of that interface and are not a callable contract.
9. Cache and consistency
The dictionary cache key contains language, tenant, and office. Its area tag is i18n and its TTL is 30 minutes. Saving one key removes the entire translation tag rather than one affected snapshot.
The save order is database upsert, local RemoveByTag, ILocalResourceSyncNotifier, then INotificationPublisher. A late failure occurs after the database has changed, with no outbox or compensation. The sync consumer logs and suppresses invalidation failures, so one instance can remain stale until TTL expiry.
The JSON loader uses a process-local ConcurrentDictionary and scans only the top-level runtime Resources/i18n directory. The source tree currently contains no such resource files. Per-file parsing errors are silently skipped, and Refresh() has no watcher or administration wiring.
10. Lifecycle truth
TranslationStatus defines Active, NeedsReview, and Deprecated, but Save always writes Active and reads return only Active. No use case performs review, publication, deprecation, restoration, or revision history.
TranslationEntry is a public record, not a persisted aggregate and not the Save result. Save returns non-generic Result. Earlier claims about optimistic version checks or a Draft/Published aggregate lifecycle do not match the source.
11. Chapter map
- Request language and Culture covers precedence, BCP-47 validation, settings, AsyncLocal, framework localization, and attacks;
- Translation scopes and persistence covers tables, uniqueness, precedence, write identity, states, and ownership;
- Localizer, cache, and frontend resources covers API divergence, JSON, fallback, fan-out, and the empty-resource defect;
- Testing, operations, and commercial GA covers evidence, capacity, metrics, recovery, migrations, and release blockers.
12. Commercial-GA red lines
- Fix the empty frontend resource response and unify single-key, batch, and bundle fallback semantics;
- honor q weights, enabled-language allowlists, canonical tags, and explicit region fallback;
- provide tenant language initialization and a governed management lifecycle;
- validate tenant, office, language, and scope combinations before writes;
- protect platform translation management with a dedicated permission, trusted identity, approval, and audit;
- provide a real review/publish/history model or intentionally simplify and remove misleading states;
- use transaction plus outbox so cache and frontend notifications are replayable and observable;
- define packaging, validation, conflict, reload, and failure behavior for JSON resources;
- define parameters, plurals, gender, time-zone, currency, and HTML-safety contracts;
- gate GA on dual-ORM, real-HTTP, multi-instance, concurrency, failure, capacity, and recovery evidence.
13. Source navigation
# Generated routes, request-language parsing, and single/batch localizer code.rg -n "GenerateEndpoint\(|ResolveFromHeader|GetString\(|GetStringsAsync" \ src/Platform/I18n src/Hosts/BitzOrcas.Api/Middleware/LanguageResolutionMiddleware.cs -g '*.cs'
# Tables, uniqueness, the MasterData physical owner, and scope precedence.rg -n "SysTranslation|SysLanguage|BitzIndex|OrderBy\(t => t.ScopeType\)" \ src/Platform/I18n src/Platform/MasterData -g '*.cs'
# These target capabilities should currently have no matches.rg -n "SetDefaultLanguage|PublishTranslation|ExpectedVersion|MessageFormat|Outbox" \ src/Platform/I18n -g '*.cs'