I18n speaks in terms of translations, but MasterData Infrastructure owns the physical record types. Public contracts do not expose those persistence classes, yet the Infrastructure-to-Infrastructure reference and governance declaration still disagree.
1. Owner of the two tables
SysTranslationCatalogRecord and SysLanguageCatalogRecord live under BitzOrcas.Platform.MasterData.Infrastructure.Persistence. I18n Infrastructure directly references that project and creates IEntitySet<T> adapters around those types.
The governance marker declares only Authorization and omits MasterData. A durable design should move these records and mappings to the I18n owner or let MasterData expose a narrow public port and then make governance match reality.
2. SysTranslation fields
| Field | Current contract |
|---|---|
| TranslationKey | Required, maximum 300 |
| LanguageCode | Required, maximum 20, not canonicalized |
| ScopeType | int expected to be 0/1/2, with no visible database check constraint |
| TenantId | Inherited; Platform writes "0" |
| OfficeId | Inherited string; no office writes "0" |
| Context | Optional, maximum 200, not used for selection |
| Value | Required, maximum 2,000 |
| Status | int; reads accept only Active=0 |
| IsDeleted/DeleteTime | Inherited soft-delete metadata |
The table declares tenant and soft-delete metadata. The read-model predicate does not explicitly mention IsDeleted. Dual-ORM tests must prove the global filter rather than documentation inferring behavior from attributes alone.
3. Business uniqueness
The unique index is:
TranslationKey + LanguageCode + ScopeType + TenantId + OfficeIdIt provides the final guard for upsert. Context, status, and version do not participate. Platform and Tenant rows must keep OfficeId at "0"; malformed nonzero values can otherwise bypass logical uniqueness. Language casing can also produce duplicates depending on database collation.
4. Scope encoding
TranslationScope fixes Platform=0, Tenant=1, and TenantOffice=2. The repository assigns TenantId "0" for Platform and current tenant or "0" otherwise. It retains current OfficeId only for TenantOffice and writes "0" for the other scopes.
// Intended rule: tenant-owned scopes require a real tenant.if (scope is TranslationScope.Tenant or TranslationScope.TenantOffice && string.IsNullOrWhiteSpace(currentTenantId)) return Errors.TenantRequired();
// An office override must not degrade into OfficeId=0.if (scope is TranslationScope.TenantOffice && currentOfficeId is null) return Errors.OfficeRequired();
// Platform writes should use a dedicated trusted-platform policy.await platformPolicy.EnsureCanManageTranslationsAsync(currentActor, cancellationToken);This target guard is now enforced. The handler rejects context-missing writes before reaching the repository: a non-Platform scope without a trusted tenant returns I18nErrors.TranslationInvalidInput; a TenantOffice scope without a valid OfficeId fails likewise. The Platform-scope writer identity is covered in §9.
5. Candidate query
The store filters language, Active status, and optional keys or prefix. When TenantId is present, it reads only the current tenant and "0"; when TenantId is absent, it adds no tenant predicate and can read rows for every tenant before merging. A no-tenant caller is therefore a high-risk path.
An office row participates only when its OfficeId matches. Tenant and Platform rows are not checked for their canonical OfficeId/TenantId combinations, so damaged rows can affect precedence.
6. Winner algorithm
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// Ascending scope: Platform, then Tenant, then Office.foreach (var row in rows.OrderBy(item => item.ScopeType)){ // Office rows participate only when they match the current office exactly. if ((TranslationScope)row.ScopeType == TranslationScope.TenantOffice && row.OfficeId != effectiveOfficeId) continue;
merged[row.TranslationKey] = row.Value;}The unique index normally makes the result deterministic inside each scope. Invalid ScopeType values, case duplicates, soft-delete differences, or malformed tenant/office combinations can still make the outcome provider-dependent.
7. Prefix and explicit keys
When keys are nonempty, the store filters by that collection and ignores KeyPrefix. Empty keys mean “all,” and only then is Prefix pushed down. Whether StartsWith is case sensitive and index-friendly depends on provider and collation.
An empty user-controlled prefix retrieves the entire language dictionary. Endpoints need prefix length and grammar checks, response bounds, and a permission decision about who may export a complete tenant language pack.
8. SaveTranslation transaction window
Save checks that Key, LanguageCode, and Value are nonblank, then upserts. Input normalization is centralized in I18nRequestInput: values are trimmed and length caps enforced (Key 300, LanguageCode 20, Value 2000, Context 200); BCP-47 validity is checked via CultureInfo.GetCultureInfo (a parse failure returns failure); the Scope enum is constrained with Enum.IsDefined. Invalid input is rejected before reaching the repository, rather than relying on the ORM or database to fail later.
There is no explicit unit of work, transaction, or outbox around the sequence. sourceVersion is the current Unix millisecond rather than a database revision. The notification field named keyPrefix contains the full key.
9. Platform-write identity
Platform-scope writes are no longer gated on caller type alone. The handler now requires the caller to satisfy both conditions: hold the WellKnownPermissionCodes.RootCrossTenant permission and pass ActorKey.TryCreatePlatformOperator as a stable Host/System platform operator. When both hold, the handler constructs a RootCrossTenantOperation.Authorize(user, RootCrossTenantOperations.I18nPlatformTranslationManage, ...) capability ticket and passes it to the repository, which re-validates the operation code i18n.platform-translation.manage via rootOperation.EnsureOperation(...). The error message is explicit: only a stable Host/System platform operator holding the cross-tenant permission may write Platform-level translations.
Platform text affects every tenant and is shared supply-chain configuration. The implementation has tightened from “any Application caller may write” to a triple check of cross-tenant permission, platform-operator identity, and an audit capability ticket.
10. TranslationStatus is not a publication workflow
The enum declares Active, NeedsReview, and Deprecated. Save always writes Active, and read paths return only Active. There is no command to submit review, approve, publish, deprecate, or restore.
If immediate publication is intentional, remove misleading workflow states and document that choice. A commercial content operation instead needs Draft Revision and Published Projection with author, reviewer, timestamps, reason, diff, and rollback target.
11. SysLanguage catalog
The language table is unique by (TenantId, Code) and stores Name, DisplayName, Icon, IsDefault, and IsDisabled. The read model requires exact current TenantId and does not merge a platform catalog.
The MasterData CSV contains zh-CN and en-US with blank TenantId, while the seeder matches only Code. That treats a tenant-marked table as global owner data. The seed, query, and per-tenant uniqueness semantics do not yet form a complete provisioning model.
12. Concurrency and idempotency
Upsert performs read-before-insert/update with no expected version. Two concurrent first writes can both miss and let uniqueness fail one request; the handler does not reread a unique conflict into an idempotent success. Concurrent updates can silently last-write-wins.
GA needs ETag/ExpectedVersion or request idempotency. Database upsert and conflict mapping must expose the same Result/Problem Details behavior across SqlSugar and EF Core.
13. Test matrix
| Scenario | Current evidence | GA addition |
|---|---|---|
| Three-scope precedence | Basic dual-ORM parity | Invalid rows and every supported database |
| Unique key | Metadata and schema init | Concurrent first write, casing, soft-delete recreation |
| Language list | Handler unit and parity | Seed, new tenant, platform merge policy |
| Platform caller | RootCrossTenant + platform operator + capability ticket | Real HTTP identity and permission matrix |
| Missing tenant/office | Handler fail-closes | Zero-side-effect evidence under concurrency and dirty combinations |
| Input validation | Trim + length + BCP-47 + Scope enum | Collation and dual-ORM consistency |
| Status | Active query | Real publication workflow or simplification |
| Save notifications | No failure injection | Failure at DB/cache/sync/notification stages |
| Multi-ORM | Scenario parity | Collation, prefix, and concurrency error contract |
14. Inspection commands
# Physical owner, unique key, and precedence implementation.rg -n "SysTranslationCatalogRecord|UX_SysTranslation|effectiveTenant|OrderBy\(t => t.ScopeType\)" \ src/Platform/I18n src/Platform/MasterData -g '*.cs'
# Publication, audit, concurrency, and outbox should currently have no matches.rg -n "PublishTranslation|ReviewedBy|ExpectedVersion|Outbox|IdempotencyKey" \ src/Platform/I18n -g '*.cs'I18n overview · Localizer, cache, and resources · MasterData module