Summary
transliterate and transliterate_tokens disagree on unsupported input.
transliterate is strict: a single character outside the sanitizer's allowed set makes it
return the entire input unchanged. transliterate_tokens carries no such guard — it renders the
supported tokens and passes the rest through.
input transliterate transliterate_tokens(tokenize(input))
----- ------------- -------------------------------------
"ké" "ké" "কé"
"k~a" "k~a" "ক~আ"
"日k" "日k" "日ক"
Why it matters
CLAUDE.md documents the strictness contract for transliterate and the drop-then-render contract
for transliterate_lenient, but says nothing about transliterate_tokens. Yet the token path is
what incremental integrations use — src/wasm/mod.rs and any keyboard that tokenizes once and
re-renders the active word. Those callers silently get partial transliteration where the text API
would have refused.
Note this is not the tokenizer's fault: tokenize faithfully reassembles its input. The asymmetry
is that render_text_inner::<CHECK_INPUT = true> validates each character while render_tokens
has no equivalent.
How it was found
Adding a dirty corpus (inputs containing characters the sanitizer rejects) to the property tests.
The transliterate(x) == transliterate_tokens(tokenize(x)) property held over 45k clean inputs and
broke immediately on unsupported ones.
The same exercise showed the lenient == strict(clean) property had been vacuous: every input
in the clean corpus already satisfies clean(x) == x, so the assertion never had teeth. It now
asserts a minimum number of inputs actually had something to clean.
Options
- Make
transliterate_tokens strict too — return the input unchanged if any token contains an
unsupported character. Consistent, but changes the token API's behavior and may break the
incremental keyboard path, which arguably wants best-effort rendering.
- Document the split explicitly —
transliterate is strict, transliterate_tokens is
best-effort, transliterate_lenient drops. Add transliterate_tokens_checked for callers that
want strictness.
- Leave as is, with the behavior pinned by a test.
Currently (3): tests/property_tests.rs::strict_text_path_and_token_path_diverge_on_unsupported_input
pins it so any change is deliberate. Deciding between (1) and (2) is a public-API call.
Summary
transliterateandtransliterate_tokensdisagree on unsupported input.transliterateis strict: a single character outside the sanitizer's allowed set makes itreturn the entire input unchanged.
transliterate_tokenscarries no such guard — it renders thesupported tokens and passes the rest through.
Why it matters
CLAUDE.mddocuments the strictness contract fortransliterateand the drop-then-render contractfor
transliterate_lenient, but says nothing abouttransliterate_tokens. Yet the token path iswhat incremental integrations use —
src/wasm/mod.rsand any keyboard that tokenizes once andre-renders the active word. Those callers silently get partial transliteration where the text API
would have refused.
Note this is not the tokenizer's fault:
tokenizefaithfully reassembles its input. The asymmetryis that
render_text_inner::<CHECK_INPUT = true>validates each character whilerender_tokenshas no equivalent.
How it was found
Adding a dirty corpus (inputs containing characters the sanitizer rejects) to the property tests.
The
transliterate(x) == transliterate_tokens(tokenize(x))property held over 45k clean inputs andbroke immediately on unsupported ones.
The same exercise showed the
lenient == strict(clean)property had been vacuous: every inputin the clean corpus already satisfies
clean(x) == x, so the assertion never had teeth. It nowasserts a minimum number of inputs actually had something to clean.
Options
transliterate_tokensstrict too — return the input unchanged if any token contains anunsupported character. Consistent, but changes the token API's behavior and may break the
incremental keyboard path, which arguably wants best-effort rendering.
transliterateis strict,transliterate_tokensisbest-effort,
transliterate_lenientdrops. Addtransliterate_tokens_checkedfor callers thatwant strictness.
Currently (3):
tests/property_tests.rs::strict_text_path_and_token_path_diverge_on_unsupported_inputpins it so any change is deliberate. Deciding between (1) and (2) is a public-API call.