Skip to content
bitzorcas
中EN

Concept

I18n internationalization and runtime localization

A source-verified I18n overview covering request-language resolution, platform/tenant/office translation overrides, database and JSON fallback, per-instance cache synchronization, frontend resource projection, and the current commercial-GA gaps.

Last updated

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 global L() AsyncLocal;
  • Platform, Tenant, and TenantOffice translation 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;
  • SaveTranslation upsert, 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

HTTP request

LanguageResolutionMiddleware
header · cookie · query · setting

CultureInfo + ILanguageContext + L()

Three queries + one save endpoint

LocalizerService

SysTranslation
MasterData owner

Resources/i18n/*.lang.json

ICacheStore
30 minutes · i18n tag

LocalResourceSync
Translation

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 routeResource / actionCurrent behavior
GET /api/i18n/languagesi18n/languages / ViewReads enabled or all languages for the current tenant
GET /api/i18n/translationsi18n/translations / ViewMerges database translations for language/current tenant/office and can push down KeyPrefix
GET /api/i18n/resourcesi18n/translations / ViewIntended frontend bundle; currently empty because of the no-key batch path
POST /api/i18n/translationsi18n/translations / UpdateSaves 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

yesno

Key + language + tenant + office

Read Active rows only

Platform: TenantId = 0

Tenant: current TenantId overrides

TenantOffice and matching OfficeId?

Office value overrides

Keep tenant or platform value

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

Save the current office's button label
// 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

yesnoyesnoyesnoyesno

GetString(key, requestedLanguage)

Requested-language DB hit?

Return value

Requested-language JSON hit?

Read fallbackLanguage setting
default en-US

Fallback-language DB hit?

Fallback-language JSON hit?

Return final segment of key

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

12. Commercial-GA red lines

  1. Fix the empty frontend resource response and unify single-key, batch, and bundle fallback semantics;
  2. honor q weights, enabled-language allowlists, canonical tags, and explicit region fallback;
  3. provide tenant language initialization and a governed management lifecycle;
  4. validate tenant, office, language, and scope combinations before writes;
  5. protect platform translation management with a dedicated permission, trusted identity, approval, and audit;
  6. provide a real review/publish/history model or intentionally simplify and remove misleading states;
  7. use transaction plus outbox so cache and frontend notifications are replayable and observable;
  8. define packaging, validation, conflict, reload, and failure behavior for JSON resources;
  9. define parameters, plurals, gender, time-zone, currency, and HTML-safety contracts;
  10. gate GA on dual-ORM, real-HTTP, multi-instance, concurrency, failure, capacity, and recovery evidence.

13. Source navigation

Terminal window
# 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'

Module catalog · MasterData module · Authorization module

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%