◌ Analysis SEV-3 technologydata

Silent locale data loss

The unowned seam

A build that merged locale files from two frontends kept only the last writer's value for any key the two shared, dropping the other silently. There was no error and no crash; the loss surfaced only as missing strings in one language, caught by hand. The proximate cause is a shallow merge that never asserted its two inputs had disjoint keys. The systemic cause is that the seam between the two frontends' locale ownership belongs to neither team — Conway's law expressed as a silent data path.

Severity
SEV-3
Blast radius
one locale namespace
Locale merge data path Two frontends emit overlapping locale keys into one shared namespace; a shallow last-key-wins merge silently drops the overlapping keys before the bundle is emitted. frontend A emits locale keys frontend B emits locale keys shallow merge last key wins emitted bundle missing A's keys overlapping keys dropped silently
Locale merge data path. Two frontends emit overlapping keys into one shared namespace; a shallow last-key-wins merge (dashed accent) silently drops the overlap before the bundle is emitted.

Timeline · UTC

build · collect two frontends emit locale JSON for the same namespace build · merge shallow object merge keeps only the last writer for shared keys build · emit overlapping keys from the first frontend are dropped, no warning review missing strings caught by hand in one locale

A defect class, not an outage

This is a reconstruction of a recurring defect, not the report of a single dated event. The pattern has appeared independently in more than one codebase that splits a product across two frontends in one repository, and the account below is assembled from that shape rather than from one timeline. The particulars — which language went blank, which build ran — vary; the mechanism does not. Where the reconstruction infers sequence it cannot directly observe, that is marked. The aim is to name the conditions under which the defect recurs, so a reader can recognize the seam in their own build before it drops anything.

What happened

At build time, two frontends each emit a file of locale strings — key-value pairs, one per translatable label — for the same locale and the same namespace. A merge step folds those files into one object so the application can load a single bundle per language. The merge is shallow: it assigns the entries of the second object over the entries of the first, so that for any key the two objects share, the second writer’s value lands and the first writer’s value is discarded. The output is a single, well-formed object. It is valid; it is just smaller than the union of its inputs by exactly the count of the keys they had in common.

The discarded entries do not announce themselves. The merge does not fail, the build does not warn, and the emitted bundle parses cleanly. RFC 8259 is explicit that JSON imposes no requirement on how duplicate names within an object are handled — implementations may take the first, take the last, or report an error, and behavior is unpredictable across them (RFC 8259 §4). A last-writer-wins object merge is one lawful resolution of that latitude. Nothing in the format treats the collision as exceptional, so nothing downstream has cause to.

Why it hid

The defect has no signal. A crash has a stack trace; a malformed file has a parse error; a missing file has a path that does not resolve. This has none of these. The merge succeeds, the bundle is structurally complete, and every key present resolves to a string that renders. The only evidence that anything was lost is the absence of a key that no longer exists to be looked up — and a lookup for a missing key returns a fallback or an empty label, not an error. The internationalization layer that consumes the bundle is specified to resolve and format strings for a requested locale (ECMA-402); it is not specified to know which keys ought to have been there. Completeness of the catalogue is the build’s responsibility, and the build asserted none.

So the detector is a person. The loss surfaces when someone fluent in the affected language opens the affected screen and sees a blank where a label belongs, or sees the fallback language bleed through. That detector is slow, partial, and lucky — it finds only the strings someone happens to look at, in the locales someone happens to read. (A neighboring hazard travels the same silent path: where keys or values are normalized inconsistently, two strings that look identical can compare as distinct, and a merge or lookup can lose one of them without a collision ever being visible — the normalization forms exist precisely to make such comparisons well-defined (Unicode Standard Annex #15).)

The contributing layers

Read from the surface down, the defect is layered, and each layer is individually reasonable. The proximate layer is the merge: a shallow assign written for the common case in which two inputs have disjoint keys, and correct exactly while that assumption holds. It never asserted the assumption it depended on, so the day the assumption broke, it broke quietly rather than loudly.

Beneath that is the data layer: two frontends own overlapping slices of one locale namespace, so the inputs are not in fact disjoint. And beneath that is the visibility layer: there is no point before the merge where the two inputs are diffed against each other, so the overlap is invisible until the merge resolves it — by which time the resolution has already discarded one side. The merge is both the first place the two inputs meet and the place that loses the conflict.

The deepest layer is structural, and it is where this analysis stops. No role was assigned the seam between the two frontends’ locale ownership. Each team owned its own strings and tested its own surface; neither owned the union, the namespace they shared, or the merge that joined them. The build inherited its shape from the org chart — two teams, two frontends, one accidental shared namespace and no owner for it — which is the observation Conway made in 1968: a system designed by an organization is constrained to mirror that organization’s communication structure (Conway, 1968). Here the boundary between two teams’ communication became a boundary in the data, and because it was a boundary no one was responsible for, it became a silent data path. The fix that holds is not a better merge but an owned seam: a step that treats a shared key as a conflict to be reported rather than a value to be overwritten, owned by someone accountable for the union. The five whys below trace the same descent in order.

Key resolution states A locale key moves from present, to overwritten when a later object carries the same key, to dropped when the earlier value is discarded. present overwritten dropped same key, later object earlier value discarded
Key-resolution states. A shared key moves from present, to overwritten when a later object carries it, to dropped when the earlier value is discarded.
why1
Why were UI strings missing in one language?
A shallow merge of two locale objects kept only the last writer's value for any key the two shared.
why2
Why did two objects share keys?
Two frontends in one repository own overlapping slices of the same locale namespace.
why3
Why did no one see the overlap?
There is no diff between the two frontends' locale inputs; the merge is the first place they meet, and it is lossy.
why4
Why is the merge lossy by default?
The merge step was written for the common case of disjoint keys and never asserted disjointness.
why5
Why was disjointness never asserted?
The seam between the two frontends' locale ownership is owned by neither team — Conway's law, expressed as a silent data path.

Sources

  1. primaryHow Do Committees Invent? (Conway, 1968)
  2. primaryRFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
  3. primaryUnicode Standard Annex #15 — Unicode Normalization Forms
  4. primaryECMA-402 — ECMAScript Internationalization API Specification