Build1 publisher3 min readPublished
Parlotype's localization gate landed before roughly 450 keys left the markup
A parity script, an xUnit mirror of it, two Claude Code hooks and a skill file all went in before any hardcoded string moved, and a JSON baseline of per-file literal counts is what let a half-finished extraction still pass.
The Engineer · Build desk

What happened
- Parlotype's UI was English-only, with 211 literal attributes across 26 .axaml files and roughly 200 string literals across 48 view models, plus the tray menu, the dialogs and the toasts.
- The developer ran the job as a directed Claude Code session, making the architectural calls and reviewing everything while the agent did the extraction, the translations, the tests and most of the implementation.
- The merged result on master is 389 keys in three languages, with 122 files changed and a diff of +11,467 / -618.
- The parity script, an xUnit mirror of it, a PostToolUse hook, a Stop hook and a skill file all landed in phase 2, before a single one of the roughly 450 keys moved.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Baseline literal counts may shrink and never grow, so an agent halfway through extraction cannot pass the check by adding new literals to a file that was already dirty.
- exposure The hooks only fire on sessions that go through Claude Code, so a human editing markup in an IDE is covered by the xUnit facts and the release gate or not at all.
- decision The guard exits 0 whenever it fails internally, so anyone copying this pattern accepts a silent miss on the days the tripwire itself is broken.
The checker is four file rules and a counter. `scripts/check-localization.ps1` verifies key parity across locales, placeholder parity (the `{0}` counts must match, or `string.Format` throws in front of a user), that every `{loc:Tr}` key in markup resolves, and it scans `.axaml` files for hardcoded literals that have not moved yet [11]. The counter is `scripts/localization-baseline.json`, which records how many hardcoded literals each `.axaml` still has [15].
Both cases are spelled out in the hook. A stale baseline with nothing broken tells the agent to re-baseline; a new literal, a missing key or a placeholder mismatch exits 2 and blocks [16]. Exit code 2 is what makes a Claude Code hook blocking, and the same guard exits 0 on any internal failure of its own, so a bug in the tripwire cannot wedge a session [17]. I think that default is right. A guard able to hang the session it is protecting gets switched off within a day.
The ordering argument is about cost. An agent doing bulk extraction across 26 markup files will drift: miss an attribute, invent a key naming scheme halfway through, translate one locale and forget the other [10]. "A human reviewer catches that at review time, which is the expensive moment. A checker catches it in the loop, which is free," Parlotype's developer wrote in the dev.to post [9][26]. He also broke each check on purpose before trusting it, deleting a key from one locale, adding a literal to markup, mismatching a placeholder, and says that habit is what exposed the first blind spot [18].
One architectural call had nothing to do with strings. `x:CompileBindings="True"` is mandatory in this codebase and `{ReflectionBinding}` is banned, but live language switching wants a binding to a localizer lookup, exactly the indexer-or-method shape the ban targets, and he went in expecting to write a narrow documented exemption [19]. Avalonia 12 removed the need for one: `CompiledBinding.Create<TIn, TOut>` takes an expression and a source object, so a plain property access on a known type compiles [20]. `Localizer.Entry(key)` returns one cached `LocalizedString` per key, an `ObservableObject` whose `Value` re-reads the `ResourceManager` at the current culture, and that per-key object exists so the binding expression can be `s => s.Value` instead of an indexer [21].
Some sizing, since the post gives enough to do it. 389 keys in three languages is 1,167 translated entries [22]. The diff nets 10,849 lines across the 122 files, about 89 each [23]. The starting inventory of 211 literal attributes and roughly 200 view-model literals comes to about 411, against the roughly 450 keys the guardrails were built to precede [24].
The post says three user-visible defects got past all four guardrails, that each was a different category of blind spot, and that none was a translation error [6]. It does not identify them in the sections that describe the guardrails. The reach of those checks is bounded by what they read: all four open files, so a defect that only exists once a window is rendered sits outside them by construction [25].
What to watch
- The follow-up naming the three defects would show whether any of them was catchable by a check that only reads files.
- Whether the ninth language really costs one translation file and nothing else, the goal stated at the outset.
- Any change to the signature of CompiledBinding.Create in Avalonia lands directly on the TrExtension that replaced the exemption.