Skip to content

fix: don't create duplicate languages from retired ISO 639-3 codes (BL-15916) - #154

Merged
hatton merged 3 commits into
mainfrom
BL-15916-retired-iso-code-duplicates
Jul 29, 2026
Merged

fix: don't create duplicate languages from retired ISO 639-3 codes (BL-15916)#154
hatton merged 3 commits into
mainfrom
BL-15916-retired-iso-code-duplicates

Conversation

@andrew-polk

@andrew-polk andrew-polk commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #149. Found while auditing the QA test steps on the card: the langtags.json API 1.4 update introduced a user-visible bad language entry that no test step covered.

The bug

langtags.json sometimes keeps a code that ISO has since retired in an entry's iso639_3 field even though the entry's own tag already uses the surviving code — e.g. tag: "enm-Latn-IE" with iso639_3: "yol", or tag: "jkp-Zyyy-MM" with iso639_3: "kpp". Because the pipeline keys generated entries off iso639_3, we filed those under the retired code and minted a second, duplicate card for a language we already list under its current code.

Where the surviving code belongs to a different language, the duplicate also inherited the wrong name and subtag. That was the user-visible symptom: yol (Yola, a language of Ireland) was showing up in the language chooser as:

Middle English (1100-1500) — A language of Ireland

carrying the language subtag enm. Three consequences, all confirmed in the running react-mui demo:

  • selecting it would have written the tag enm onto a collection;
  • searching "Yola" found nothing at all;
  • enm is on our deliberate historic-language exclusion list, but because the entry was keyed yol it slipped past the filter — so we were showing a historic language we intend to suppress.

The new iso-639-3.tab that came with the API 1.4 update retires yol, which is what triggered it.

The fix

In parseLangtagsJson: when an entry's iso639_3 code is no longer in iso-639-3.tab but the tag's language subtag is, trust the tag. Entries whose retired code matches their own tag (33 of them, e.g. aoh/Arma) are deliberately left alone — there is no surviving code to fold them into, and they behave exactly as before.

Regenerating languageData.json removes exactly five duplicates, folding their regions, scripts and tags into the surviving entries and changing nothing else across the other 7,953:

Removed duplicate Folded into
yol "Middle English (1100-1500)" [enm] enm
kpp Paku Karen [jkp] jkp
jeg Oy [oyb] oyb
dek Suma [sqm] sqm
tpw Tupinambá [tpn] tpn

Retired codes stay searchable via alternativeTags, so kpp, jeg, dek, tpw (and skk, thx) still find the right language.

Yola is no longer offered at all. The current iso-639-3.tab has retired yol, and no source we have still carries the name "Yola", so there is no correct entry we can build for it. Suppressing it beats offering it under another language's name.

Verification

  • find-language 191 tests, language-chooser-controller 97, language-chooser-react-hook 16, language-chooser-react-mui e2e 114 — all pass.
  • tsc typecheck clean; eslint 0 errors.
  • Two new tests in languageSearch.spec.ts: one covering all five retired/surviving pairs (no duplicate entry, retired code still folded in and still searchable), one guarding the specific enm/yol symptom.
  • Checked by hand in the running react-mui demo that "Middle English" no longer offers the Ireland card and that "Paku Karen" returns a single card.

Also on the card, not in this PR

Auditing the test steps turned up three that were simply wrong (they asked testers to confirm country lists on the English and French cards, which are stripped on purpose; and to search for a "Kurdish Sign Language" that does not exist in the data). Those have been corrected on BL-15916, and the newly-added dyl Bhutanese Sign Language entry reading "A language of Turkey" has been reported upstream — that one is upstream source data, not ours.

Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-15916

🤖 Generated with Claude Code


This change is Reviewable


Devin review

andrew-polk and others added 3 commits July 29, 2026 12:28
… codes

https://issues.bloomlibrary.org/youtrack/issue/BL-15916

langtags.json sometimes keeps a code that ISO has since retired in an entry's
iso639_3 field even though the entry's own tag already uses the surviving code,
e.g. tag "enm-Latn-IE" with iso639_3 "yol", or tag "jkp-Zyyy-MM" with iso639_3
"kpp". We were filing those entries under the retired code, which minted a
second, duplicate card for a language we already list under its current code.

Where the surviving code belongs to a *different* language the duplicate also
inherited the wrong name and subtag. That was user-visible: yol (Yola, a
language of Ireland) was showing up as "Middle English (1100-1500) - A language
of Ireland" carrying the subtag enm. Selecting it would have written the tag enm
onto a collection, searching "Yola" found nothing, and because the entry was
keyed yol it slipped past the deliberate enm historic-language exclusion in
defaultExcludedHistoricLanguages.ts, so we showed a historic language we mean to
suppress. This surfaced when the langtags.json API 1.4 update brought in an
iso-639-3.tab that retires yol.

So when an entry's iso639_3 code is no longer in iso-639-3.tab but its tag's
language subtag is, trust the tag. Entries whose retired code matches their own
tag (33 of them, e.g. aoh/Arma) are left alone - there is no surviving code to
fold them into and they behave as before.

Regenerating languageData.json removes exactly five duplicates, folding their
regions, scripts and tags into the surviving entries and nothing else across the
other 7,953 entries:

  yol "Middle English (1100-1500)" [enm] -> enm
  kpp Paku Karen                  [jkp] -> jkp
  jeg Oy                          [oyb] -> oyb
  dek Suma                        [sqm] -> sqm
  tpw Tupinamba                   [tpn] -> tpn

The retired codes stay searchable via alternativeTags, so kpp/jeg/dek/tpw (and
skk/thx) still find the right language. Yola is no longer offered at all: the
current iso-639-3.tab has retired yol and no source we have still carries the
name "Yola", so there is no correct entry we can build for it.

Verified: find-language 191 tests, language-chooser-controller 97,
language-chooser-react-hook 16, language-chooser-react-mui e2e 114 - all pass.
tsc typecheck clean. Checked in the running react-mui demo that "Middle English"
no longer offers the Ireland card and that "Paku Karen" returns a single card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…test messages

https://issues.bloomlibrary.org/youtrack/issue/BL-15916

Two test-only follow-ups found while preflighting the retired-ISO-code fix.

scrollListToLanguageCard gave up as soon as the results list could not scroll
any further, but search results arrive in batches - so "we reached the bottom"
does not mean the card is absent, it may just not have streamed in yet. Under
load (full suite running) that raced and "Search with typos gets fuzzy matches"
failed intermittently; the same test passed 3/3 when run alone. Sweep from the
top repeatedly until the card mounts or a 15s budget runs out, instead of one
sweep with an early break. Full e2e suite now 114/114 twice in a row.

Also reworded two assertion messages in the new retired-ISO-code tests. The
first said searching "yol" "should still find enm", which is true of the raw
search but not of what a user sees - defaultSearchResultModifier then filters
enm out as a historic language. Say "raw search ... should still reach" and note
the distinction, so the next reader is not misled into thinking Yola is
reachable in the UI. Reordered the second test's loop to put yol first, since
that is the assertion actually carrying the regression guard.

No behavior change; no production code touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
https://issues.bloomlibrary.org/youtrack/issue/BL-15916

Comments only - no logic change, and regenerating languageData.json produces a
byte-identical file.

Devin's review of the retired-ISO-code fix raised two informational notes worth
recording where the next person will see them:

- addOrCombineLangtagsEntry does not overwrite `exonym` on the combine path, so a
  folded retired/surviving pair keeps whichever entry langtags listed first. All
  five current pairs carry the same `name` in langtags so it makes no difference
  today, but a future pair with differing names listed retired-first would inherit
  the retired language's name - exactly the symptom this block prevents. Noted in
  langtagProcessing.ts as something to re-check after a langtags update.

- scrollListToLanguageCard now deliberately sweeps until its timeout, so using it
  to assert a card is *absent* would burn the full 15s. Both current callers assert
  presence; noted in e2eHelpers.ts so it stays that way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@andrew-polk

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5 from Andrew Polk's machine during preflight] Consulted Devin on 2026-07-29 up to commit 6b43348.

Clean: no Bugs and no Investigate flags, so nothing was mirrored as a review thread. Four Informational items, and it is worth saying what they were rather than just counting them:

  • Two were confirmations rather than concerns — that the retired-code remap only fires on codes genuinely missing from iso-639-3.tab (leaving the 33 aoh-style entries alone), and that macrolanguage codes are correctly excluded from it.
  • One noted that when a retired-code entry folds into its surviving code, the merged entry keeps whichever name langtags happened to list first. That cannot affect any of the five current pairs — both entries in each pair carry the same name — but it is a latent version of the very bug this PR fixes, so it is now written down as a comment in langtagProcessing.ts telling whoever regenerates the data next to re-check the folded entries' names.
  • One noted that the reworked e2e scroll helper deliberately keeps sweeping until its timeout, so using it to assert a card is absent would waste the full 15 seconds. Both current callers assert presence; that constraint is now a comment in e2eHelpers.ts.

Both of those last two are recorded in commit 6b43348, which is comments only — regenerating languageData.json afterwards produces a byte-identical file.

CI (run-tests) passed for this commit. This repo has no comment-posting review bots configured (no Greptile/CodeRabbit), so Devin and CI are the full remote panel.

@andrew-polk
andrew-polk marked this pull request as ready for review July 29, 2026 20:51
@hatton
hatton merged commit b4033a0 into main Jul 29, 2026
1 check passed
@hatton
hatton deleted the BL-15916-retired-iso-code-duplicates branch July 29, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants