I18n does not reach GA merely because one endpoint can return Chinese text. It must produce explainable results across tenants, offices, instances, databases, deployment versions, and failure stages, while translation operations remain safe for the entire product.
1. Current automated evidence
| Test surface | Existing evidence |
|---|---|
| Query handlers | Enabled-language list, store-failure propagation, translation prefix call |
| Localizer | Store failure falls back to the final key segment |
| Architecture | Queries depend on read-model stores, production defaults fail closed, generated adapters exist |
| Dual-ORM parity | Platform/Tenant/Office precedence, deletion fallback to Platform, enabled/all/default languages |
| API shell smoke | Read-model stores resolve and can be called from the composition root |
There are no tests for LanguageResolutionMiddleware, SaveTranslation, successful GetI18nResources, JSON loading, HTTP authorization, TranslationCacheSyncConsumer, real multi-instance consistency, concurrency, failure injection, capacity, or recovery.
2. Minimum source regression suite
# Keep three I18n evidence surfaces explicit instead of treating broad parity as a unit test.I18N_APP_FILTER="FullyQualifiedName~I18n"I18N_ARCH_FILTER="FullyQualifiedName~I18n|FullyQualifiedName~MasterDataInfrastructure"I18N_PARITY_FILTER="FullyQualifiedName~PortRepositoryParity"
# Application tests prove deterministic handler and localizer behavior first.dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj --filter "$I18N_APP_FILTER"
# Architecture and integration tests then prove ports and both ORM adapters.dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj --filter "$I18N_ARCH_FILTER"dotnet test tests/BitzOrcas.Integration.Tests/BitzOrcas.Integration.Tests.csproj --filter "$I18N_PARITY_FILTER"These filters accelerate diagnosis. A GA pipeline must still execute the complete solution suite so I18n regressions in Settings, MasterData, Authorization, caching, and host composition are not hidden.
3. HTTP negotiation matrix
Use a real WebApplicationFactory for every Header/Cookie/Query/Setting combination, q weights, casing, unknown Culture, disabled language, and no-tenant caller. Assert response data, Content-Language, Culture, cache key, and fallback reason agree.
// Deliberately make the first header item lower quality than the second.using var request = new HttpRequestMessage( HttpMethod.Get, "/api/i18n/translations?LanguageCode=en-US");request.Headers.TryAddWithoutValidation( "Accept-Language", "fr-FR;q=0.1,en-US;q=1.0");request.Headers.Add("Cookie", "bitzorcas.lang=en-US");
var response = await client.SendAsync(request, cancellationToken);
// The target resolver reports the same canonical value in data, diagnostics, and headers.response.EnsureSuccessStatusCode();response.Content.Headers.ContentLanguage.ShouldContain("en-US");await evidence.AssertResolvedLanguageAsync( "en-US", reason: "explicit-user-preference");This demonstrates a target product contract, not current precedence. Product policy must choose and publish that precedence.
4. Authorization and tenancy matrix
Test anonymous, normal User, platform administrator, System, trusted Application, tenants A/B, and offices 1/2. Both reads and writes must prove actual Resource/Action-to-permission mapping through HTTP.
Attack cases include a no-tenant read seeing every tenant candidate, TenantOffice without office degrading to zero, an Application caller changing global text, tenant A supplying tenant B identity, disabled-language access, and an undefined integer Scope bypassing guards.
5. Resource API regression
Create complementary keys in the requested-language database, JSON, and fallback-language database. Call GetString, GetStringsAsync, GetTranslations, and GetI18nResources, then verify every public contract follows its documented source policy and Prefix cannot expose unrelated modules.
// Give each source a different key so an incorrect winner cannot hide behind equal values.await database.SaveAsync( "Identity.Login.Title", "en-US", "Sign in", cancellationToken);files.WriteJson( "Identity.en-US.json", new Dictionary<string, string> { ["Identity.Login.Submit"] = "Continue" });await database.SaveAsync( "Identity.Login.Help", "fr-FR", "Besoin d'aide ?", cancellationToken);
var resources = await api.GetResourcesAsync( "en-US", "Identity.", cancellationToken);
// Prefix applies after all sources and the configured fallback language have merged.resources["Identity.Login.Title"].ShouldBe("Sign in"); // requested DBresources["Identity.Login.Submit"].ShouldBe("Continue"); // requested JSONresources["Identity.Login.Help"].ShouldBe("Besoin d'aide ?"); // fallback DBThe test must configure fr-FR as the fallback language. It should also prove source metadata rather than infer it only from values.
6. Save failure injection
Inject a failure at repository upsert, local cache removal, sync notification, and frontend notification. At every point record database state, each instance snapshot, sync events, and the client response. Retrying must neither duplicate state nor leave permanent divergence.
The current sequence can partially succeed after Upsert. The GA target writes Translation and Outbox in one transaction and lets retryable consumers deliver cache and frontend messages after commit. The command returns a stable resource revision.
7. Multi-instance consistency
Start at least two real hosts with shared database and messaging. Warm instance A, save through instance B, and require A to observe the new value within the consistency SLO. Drop, delay, reorder, and duplicate sync events to prove version comparison and TTL convergence.
Do not test only by invoking the consumer directly. Cover routing, serialization, ResourceType, scope, retries, and poison policy. Platform edits should invalidate all affected tenants; Tenant and Office edits should not produce global stampedes.
8. Dual-ORM and database differences
For every supported database, validate unique indexes, case collation, StartsWith SQL, soft-delete filtering, concurrent upsert, long Unicode values, OfficeId string comparison, and transaction isolation. SqlSugar and EF Core must produce the same public Result/Problem Details contract.
Capture execution plans and P95 for Prefix queries. A large explicit key collection can generate an oversized IN clause; limit batch cardinality or use a provider-neutral batching strategy.
9. Capacity baseline
Suggested dimensions are 100/10,000/1,000,000 keys, 2/10/50 languages, 1/1,000 tenants, 1/100 offices, 1/1,000 concurrent callers, 1/100 keys per batch, and 10/1,000 edits per minute.
Record P50/P95/P99, scanned rows, cache hits/misses, dictionary heap size, GC, thread-pool queueing, synchronous L() blocking, broadcast delay, bundle bytes, compression ratio, and CDN hit rate. Full language dictionaries multiplied by every office can consume substantial memory.
10. Metrics and logging
i18n_language_resolution_total{source,result,fallback_reason};i18n_translation_hit_total{source,scope,language}, without raw tenant labels;i18n_missing_key_total{module,language};- cache hit/miss/evict and key cardinality;
- sync lag, consumer failure, and stale-snapshot age;
- save/review/publish/conflict/rollback;
- resource bundle keys/bytes/generation duration;
- invalid tag, disabled language, and placeholder-format errors;
- JSON load, parse, conflict, and version.
Log the key, canonical language, scope, correlation ID, and revision. Do not log Value by default; translations can contain customer branding, name templates, or sensitive operational content.
11. Alerts
Alert on repeated empty resource bundles, a spike in missing-key ratio, bulk platform translation changes, sync lag above SLO, divergent instance revisions, cache cardinality or memory growth, JSON parse failures, no-tenant reads, unusual platform-write identities, and placeholder errors.
Distinguish a safe expected missing key from database-unavailable degradation. Both can return HTTP 200 today, but only one indicates an outage.
12. Resource release process
Validate JSON schema, key naming, duplicates, coverage, placeholder parity, HTML policy, and UTF-8 at build time. Generate a manifest with resource version, source commit, languages, file hashes, and key counts.
Rolling releases can expose multiple resource versions. Return ETag or manifest version and let clients cache by version. Database overrides should record the baseline version they were authored against so upgrades can identify stale overrides.
13. Backup and recovery
Back up SysTranslation, SysLanguage, future translation revisions/published pointers, audit, and outbox. After restore, verify one default language per tenant, enabled sets, natural keys, scope combinations, published projection, soft deletion, and resource revision.
Database state and deployed JSON must be compatible. Restoring an older database under a newer resource bundle requires reconciliation for added/removed keys, obsolete overrides, placeholder changes, and conflicts.
14. Migration and rollback
Before canonicalizing LanguageCode, generate a dry-run report for casing and alias merge conflicts. Repair invalid tenant/office/scope combinations before tightening constraints; do not let a production deployment discover dirty data while creating a unique index.
When introducing publication, convert every existing Active row into an initial Published Revision and preserve ID mappings. Application rollback also needs compatible schema, resource manifest, and client cache protocol.
15. Commercial-GA blockers
GetI18nResourcescurrently returns an empty result;- single and batch JSON/fallback semantics disagree;
- negotiation ignores q values, enabled catalog, and canonical tags;
- language seeds and per-tenant catalog provisioning do not form a closed lifecycle;
- missing tenant/office and invalid scope do not fail closed;
- Platform writes rely on caller type without dedicated permission, approval, or audit;
- TranslationStatus has no real review/publication workflow;
- save and notifications lack transactional outbox, revision, and compensation;
- global tag eviction creates cross-tenant stampedes, and failed fan-out can stay stale for 30 minutes;
- JSON has no source assets, embedded loading, deterministic conflict handling, hot update, or error observability;
- parameters, plurals, rich text, time zone, and currency contracts are absent;
- HTTP, multi-instance, concurrency, failure, capacity, migration, and recovery evidence is incomplete.
16. Release-gate commands
# This empty-key bundle call must not keep discarding a loaded dictionary.rg -n "GetStringsAsync\(\s*Array.Empty<string>" src/Platform/I18n -g '*.cs'
# Target lifecycle, versioning, outbox, and negotiation capabilities.rg -n "PublishedRevision|ExpectedVersion|Outbox|ResolvedLanguage|Quality" \ src/Platform/I18n src/Hosts/BitzOrcas.Api -g '*.cs'
# I18n evidence should expand from today's narrow tests to HTTP/cache/JSON/failure suites.rg -n "I18n|LanguageResolution|JsonResourceLoader|TranslationCacheSync" \ tests -g '*.cs'I18n overview · Translation persistence · Localizer and resources