Skip to content

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

Illustration accompanying Parlotype's localization gate landed before roughly 450 keys left the markup

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.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories