afrikaans: the engine and normalizer constants move to the manifest - #756
Merged
Conversation
afrikaans.ts: final devoicing, unstressed reduction, the ⟨c⟩-softening letters, the stress-shifting loan
suffixes, and the unstressed-prefix IPA.
normalize.ts: ordinals 1-19, the multi-dot and single-dot abbreviations, the letter names, the word
acronyms and spelled-out runs, and the phonotactic onset/coda lists.
Two duplications fell out, which is the usual reason this work is worth doing:
- THE SAME SIX UNSTRESSED PREFIXES lived in three places — the stress regex, PREFIX_IPA, and the
manifest's morphology.prefixUnstressed that the shared Germanic compound engine reads. One source now,
with a throw if the IPA keys and the morphology list ever diverge.
- THE VOWEL CLASS was spelled inline twice and had already DRIFTED from vowelLetters: it was missing ⟨ö⟩,
which the diacritic table maps to [ø]. Inert today (Afrikaans emits no stress, and ⟨ö⟩ only occurs
beside another vowel so the group regex merged it either way) but one edit from mattering. Both regexes
are now built from the manifest.
⚠ ONE ACCIDENT PRESERVED DELIBERATELY. The ⟨c⟩ test was `"eiyêéè".includes(w[i+1] ?? "")`, and
includes("") is TRUE — so a WORD-FINAL ⟨c⟩ took the soft [s] by accident. A Set turns that false, which
would have flipped 38 of 1500 probe words to [k]. Kept identical, with the accident documented; the af
referee covers neither reading, so it needs its own evidence rather than a refactor's side effect.
Pure relocation: 1513 cases (random words + ordinals + normalizer strings) against main, 0 differ; af
referee unchanged at 1658/2220 (74.7%).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
- The parity throw ran at MODULE INIT, and registry.ts imports afrikaans.ts statically — so one typo in the Afrikaans manifest would have made the whole package unimportable, taking every other language and the entire suite down with it. Moved to test/afrikaans.test.ts, and verified it still fails when the two lists diverge rather than trusting that it would. - The byLen comment claimed longest-first ordering was needed so ⟨siteit⟩ beats ⟨teit⟩. It cannot be: both patterns are $-anchored and used only through .test(), so alternation order can't change a boolean. Says what it actually is now — cosmetic, kept for readability and for safety if either is ever used to capture. - The preserved ⟨c⟩ accident now points at issue #757, so the intent outlives the comment. franc→frans and arc→ars are genuinely wrong; the af referee has no word-final ⟨c⟩ at all, so it needs a phonological call rather than a refactor's side effect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
christopherthompson81
added a commit
that referenced
this pull request
Aug 8, 2026
The rule is "soft [s] BEFORE a front vowel". Word-finally there is no following vowel, so the soft
branch cannot apply — but it did, because the test was `"eiyêéè".includes(w[i + 1] ?? "")` and
includes("") is TRUE for any string. franc→frans, arc→ars, bloc→blɔs.
#756 preserved that verbatim, correctly: a data move must not change output. Deciding it needed the
phonological call, and the call is that the accident INVERTED the rule rather than extending it — no
following letter means the soft condition is unmet, not met.
⚠ NOT REFEREE-CONFIRMED, and it cannot be: af.wiktionary-af.tsv has no word-final ⟨c⟩ at all — its 20
⟨c⟩ entries are surnames (Coetzee, Terblanche), ⟨ch⟩ words, and the letter name C→sɪə. af is unmoved at
1658/2220, exactly as predicted. The goldens pin the rule being self-consistent, and say so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
christopherthompson81
added a commit
that referenced
this pull request
Aug 8, 2026
* afrikaans: a word-final ⟨c⟩ is [k], not the soft [s] (#757) The rule is "soft [s] BEFORE a front vowel". Word-finally there is no following vowel, so the soft branch cannot apply — but it did, because the test was `"eiyêéè".includes(w[i + 1] ?? "")` and includes("") is TRUE for any string. franc→frans, arc→ars, bloc→blɔs. #756 preserved that verbatim, correctly: a data move must not change output. Deciding it needed the phonological call, and the call is that the accident INVERTED the rule rather than extending it — no following letter means the soft condition is unmet, not met. ⚠ NOT REFEREE-CONFIRMED, and it cannot be: af.wiktionary-af.tsv has no word-final ⟨c⟩ at all — its 20 ⟨c⟩ entries are surnames (Coetzee, Terblanche), ⟨ch⟩ words, and the letter name C→sɪə. af is unmoved at 1658/2220, exactly as predicted. The goldens pin the rule being self-consistent, and say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr * review: the referee DOES hold a word-final ⟨c⟩ — my reason was wrong, the conclusion was not I wrote that af.wiktionary-af.tsv contains no word-final ⟨c⟩, and that its C→sɪə entry reaches output through the initialism path. Both false. The entry IS a word-final ⟨c⟩, eval.ts feeds af through phonemizeWord directly, and the initialism normalizer only fires on runs of two or more letters — so that entry traverses this exact branch, going s→k with this change. The conclusion survives: it misses both ways against sɪə, which is why the folded backbone is unmoved at 1658/2220. But the stated reason would have told the next reader that nothing exercises the branch, which is the opposite of true. My original grep was case-sensitive and lowercase-only; I then saw the C entry in later output and explained it away with an assumption I never checked. Filed the underlying gap as #761: a bare single letter is read as a sound rather than spelled as its name (B→p too), because the initialism normalizer needs ≥2 letters. Also moved the new test into the g2p describe block, where it belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both Afrikaans files carried significant data in code.
afrikaans.ts— final devoicing, unstressed vowel reduction, the ⟨c⟩-softening letters, the stress-shifting loan suffixes, the unstressed-prefix IPA.normalize.ts— ordinals 1–19, the multi-dot and single-dot abbreviations, the letter names, the word-acronym and spelled-out-run lists, the phonotactic onset/coda sets.Each lands with the reasoning it already had — including the corpus evidence in
normalize.ts, which is the part most worth keeping next to the data.Two duplications fell out
PREFIX_IPA, and the manifest'smorphology.prefixUnstressedthat the shared Germanic compound engine reads. One source now, and the engine throws if the IPA keys and the morphology list diverge (verified the throw fires).vowelLetters— missing ⟨ö⟩, which the diacritic table maps to [ø]. Inert today (Afrikaans emits no stress, and ⟨ö⟩ only ever occurs beside another vowel, so the group regex merged it either way), but one edit from mattering. Both regexes are derived now.⚠ One accident preserved on purpose
The ⟨c⟩ test was
"eiyêéè".includes(w[i + 1] ?? "")— andincludes("")istrue, so a word-final ⟨c⟩ took the soft [s] by accident. Moving the list to the manifest turns that into aSet, which isfalse— silently flipping 38 of 1500 probe words to [k].Kept byte-identical, with the accident documented in place. The
afreferee covers neither reading, so which is correct needs its own evidence rather than arriving as a refactor's side effect. Worth a follow-up; not this PR.Verification
1513 cases — 1500 random words over the full Afrikaans alphabet, the ordinal table, and 13 normalizer strings — run against
main: 0 differ.afreferee unchanged at 1658/2220 (74.7%).npm run cigreen — 228 files / 3139 tests. Manifest headers checked (non-vacuously).Spot-checked the behaviour the dot-bound comment exists to protect:
'n Chinese skipstill survives intact.🤖 Generated with Claude Code
https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr