Skip to content

Fix katakana lookup: normalize halfwidth forms to fullwidth - #106

Merged
wtetsu merged 3 commits into
wtetsu:masterfrom
abdulazizalmalki-gh:fix/katakana-segmentation
Oct 3, 2026
Merged

wtetsu merged 3 commits into
wtetsu:masterfrom
abdulazizalmalki-gh:fix/katakana-segmentation

Conversation

@abdulazizalmalki-gh

@abdulazizalmalki-gh abdulazizalmalki-gh commented Oct 3, 2026 •

Copy link
Copy Markdown

Problem

Halfwidth katakana (U+FF61-FF9F, common in manga and legacy UI text) never matches fullwidth headwords: hovering over テレビ finds nothing for the dictionary entry テレビ.

Fix

When the hovered text contains halfwidth katakana, its NFKC-normalized fullwidth form is added as an additional candidate chain, so the usual prefix decomposition runs on both spellings:

  • テレビ → テレビ / テレヒ / テレ / テ and テレビ / テレ / テ
  • カ + ゙ combining sequences fold to voiced characters (ガス → ガス)

No other behavior changes: fullwidth katakana runs are still cut into all prefixes, exactly as before (hovering ソ in ソフトウェアエンジニア still yields ソフトウェア…, ソフトウェア, ソフト, ソ), and hiragana/kanji okurigana handling is untouched. The エンジニア-part lookup and the エンジニアー long-vowel spelling also keep their original chains.

(Prior revision removed per @wtetsu's review — dropped the mid-run collapse guard to restore the show-all-plausible-candidates design.)

Tests

Added to the existing "Test Japanese words" suite: halfwidth→fullwidth headword lookup with both prefix chains, voiced-fold case, and a regression assertion that fullwidth katakana prefix cutting still works. npm test: 274/274 pass.

@abdulazizalmalki-gh
abdulazizalmalki-gh force-pushed the fix/katakana-segmentation branch from e9b6f9b to 52f6cb5 Compare October 3, 2026 08:29
@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.75%. Comparing base (1fe59fd) to head (52f6cb5).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #106   +/-   ##
=======================================
  Coverage   98.74%   98.75%           
=======================================
  Files          51       51           
  Lines        2155     2170   +15     
  Branches      415      421    +6     
=======================================
+ Hits         2128     2143   +15     
  Misses         27       27           
Flag Coverage Δ
unittests 98.75% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@wtetsu

wtetsu commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Thank you for the proposal.
I agree that supporting halfwidth katakana would be useful.

However, I'm afraid this change doesn't work as intended.

For example, with the current behavior, hovering over "ソ" in "ソフトウェアエンジニア" should show all of the following candidates at once (depending on the installed dictionaries):

  • ソフトウェアエンジニア
  • ソフトウェア
  • ソフト
  • ソ

Hovering over the "エンジニア" part should show the following (again, depending on the installed dictionaries):

  • エンジニア
  • エン
  • エ

This is the intended behavior, and this PR breaks it.

Furthermore, with a spelling like "ソフトウェアエンジニアー", nothing would be shown at all unless the dictionary contains that exact spelling.

A core design principle of Mouse Dictionary is to show all plausible candidates at once, and this change goes against that principle, so I can't merge it as is.

Halfwidth katakana (U+FF61-FF9F, common in manga/UI text) never matched
fullwidth headwords: hovering テレビ found nothing for テレビ.

When the hovered text contains halfwidth katakana, the NFKC-normalized
form is added as an additional candidate chain, so every prefix
decomposition is generated for both spellings (テレヒ -> テレ/テ and テレ/テ).
NFKC also folds voiced/semi-voiced combining sequences (カ + ゙ -> ガ).

Prefix generation for fullwidth katakana runs is unchanged: all
candidates are kept, per the show-all-plausible-candidates design.
@abdulazizalmalki-gh

Copy link
Copy Markdown
Author

@wtetsu You're right — the mid-run collapse guard broke the show-all-candidates design, and I verified it against your exact examples before reworking: with the guard, hovering ソ in ソフトウェアエンジニア yielded only the full string (no ソフトウェア/ソフト/ソ), ノーベル賞 lost ノー… i.e. every katakana prefix cut was suppressed, and ソフトウェアエンジニアー fell back to nothing beyond the raw string.

I've dropped that part entirely. The PR now does only the halfwidth normalization you agreed would be useful: for halfwidth input the NFKC form is added as a second candidate chain, so all the usual prefixes are generated for both spellings and fullwidth katakana behavior is byte-for-byte unchanged.

Confirmed against your examples on the new code (via createLookupWordsJa on the hovered run):

  • ソフトウェアエンジニア → …ソフトウェアエンジニア/…/ソフトウェア/ソフト/ソフ/ソ ✔
  • エンジニア → エンジニア/エンジニ/エンジ/エン/エ ✔
  • ソフトウェアエンジニアー → still contains ソフトウェアエンジニア ✔
  • ノーベル賞 → ノーベル賞/ノーベル/ノーベ/ノー/ノ ✔ (no more ノー suppression)

Rebased onto current master; 274/274 tests pass.

@abdulazizalmalki-gh
abdulazizalmalki-gh force-pushed the fix/katakana-segmentation branch from 52f6cb5 to 8e9eddd Compare October 3, 2026 13:57
@abdulazizalmalki-gh abdulazizalmalki-gh changed the title Fix katakana lookup: halfwidth forms + mid-run prefix collapse Fix katakana lookup: normalize halfwidth forms to fullwidth Oct 3, 2026
abdulazizalmalki-gh pushed a commit to abdulazizalmalki-gh/mouse-dictionary that referenced this pull request Oct 3, 2026
Drop the mid-run collapse guard: upstream keeps every prefix candidate
(show-all-plausible-candidates design, wtetsu's review on wtetsu#106). The
ノーベル賞 case is handled in the data layer: the ja-ar pack carries
ノーベル賞 itself as a headword, so the longest match wins and the
shorter ノー no longer misleads the lookup on its own.

Keep the halfwidth NFKC chain (テレビ -> テレビ + both prefix chains).
Matches the reworked PR wtetsu#106 head so a future merge is a no-op.
Comment thread src/main/core/entry/ja.ts Outdated
// prefix decomposition (show-all-plausible-candidates design), e.g.
// テレヒ -> テレヒ/テレ/テ plus テレビ/テレ/テ.
const chains = [str];
if (RE_HALFWIDTH_KATAKANA.test(str)) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the goal of this PR is to handle halfwidth katakana, applying normalize("NFKC") to the whole string probably causes unintended conversions in the non-katakana parts as well.

NFKC may well be useful for some characters other than halfwidth katakana. But the current code applies NFKC to the entire string as soon as it contains even a single halfwidth katakana character, which seems a bit odd.

To keep this PR focused on its goal, would it be possible to convert only the halfwidth katakana?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — that was sloppy. Reworked in 15a0ddc: the conversion now runs per character inside the halfwidth-katakana range only, so nothing else in the string gets a compatibility fold (① stays ① — whole-string NFKC would have rewritten it to "1").

The one thing I couldn't drop is the fold of カ + ゙ -> ガ. Converting U+FF9E/FF9F alone yields the combining marks ゙/ ゚, and the precomposed fullwidth letters (ガ, タ->ダ etc.) don't exist as single conversions of the halfwidth pairs, so a canonical-composition pass is needed after the scoped replace. I used NFC: it composes canonically (exactly what the voiced marks need) and, unlike NFKC, performs no compatibility mappings, so it can't touch anything outside katakana. A test now pins the scoping: "①テレビ" produces "①テレビ/①テレ/…" chains and asserts "1テレビ" is never generated.

abdulazizalmalki-gh pushed a commit to abdulazizalmalki-gh/mouse-dictionary that referenced this pull request Oct 3, 2026
The English-Gloss bridge returned the WHOLE en-ar page entry for a
gloss, which merges every homograph Wiktionary put on that English
page: ノー (loanword no) carried the archaic cattle noun نَعَم and a
duplicated interjection chunk, and arwiktionary definitions attached
as full grammar essays.

Bridging now keeps only the dominant (first) sense chunk and caps the
parenthetical definition at 80 chars in both bridge sites (JMdict
render_sense + kaikki fill_from_jawikt). Applied across the pack:
92k shard lines shortened, 0 headwords changed, en-ar pack untouched.

Code paths (src/, PRs wtetsu#106/wtetsu#107) unchanged — data layer only.
Whole-string NFKC folds unrelated characters (circled digits -> ASCII,
ligatures, fullwidth latin). Convert each halfwidth-katakana character
(U+FF61-FF9F) individually, then a canonical-composition NFC pass to
combine the converted voiced marks with the preceding letter
(カ + ゙ -> ガ). NFC performs no compatibility folds, so nothing outside
the halfwidth katakana range can change.
abdulazizalmalki-gh pushed a commit to abdulazizalmalki-gh/mouse-dictionary that referenced this pull request Oct 3, 2026
Syncs feat/arabic-packs with the PR wtetsu#106 review fix (wtetsu: whole-string
NFKC would fold unrelated characters). Per-character conversion inside
U+FF61-FF9F plus a canonical NFC composition pass for voiced marks;
nothing outside the range can change.
Comment thread src/main/core/entry/ja.ts Outdated
// (circled digits, fullwidth latin, ligatures...). The trailing NFC pass is a
// canonical-composition pass, needed to combine the converted voiced marks
// with the preceding letter (カ + ゙ -> ガ); it performs no compatibility folds.
const RE_HALFWIDTH_KATAKANA = /[\uFF61-\uFF9F]/;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent of this code isn't very clear, and it seems inefficient. Could you rewrite it?

For example, I don't see any reason to define two nearly identical regexes and use one for test() and the other for replace(). Isn't replace() alone enough? If there's no halfwidth katakana in the string, it simply does nothing.

Also, this code calls the conversion function once per character, which looks inefficient. I think this could also cause dakuten-related processing to produce incorrect results.

Wouldn't a regex like /[\uFF61-\uFF9F]+/g solve this?

"テレビ".replace(/[\uFF61-\uFF9F]+/g, (run) => run.normalize("NFKC")) // => "テレビ"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4fe574f — the run-based regex is exactly right and simpler than what I had. One regex, one NFKC call per maximal run, and the dakuten concern you raised is handled by it directly: NFKC over the run composes カ + ゙ -> ガ in the same pass, whereas my per-character version produced a decomposed カ+combining mark until a second NFC pass fixed it (and would have left ゙ following non-kanakana input decomposed).

The test()/replace() duplication is gone too: a string without halfwidth katakana comes back unchanged from replace(), so the existing converted !== str check is the only guard needed.

Behavior verified unchanged on top of this: テレビ/ガス chains as before, ①テレビ keeps ① intact, あ゙ outside a katakana run stays out of scope. 274/274 tests, biome/tsc clean.

Follow-up on review: one regex (runs, U+FF61-FF9F+), one NFKC call per
maximal run. Running NFKC per character needed a separate NFC pass to
compose voiced marks; NFKC over the whole run composes them directly
(カ + ゙ -> ガ). No test()/replace() duplication: a string without
halfwidth katakana comes back unchanged, so the chain guard is the
already-present converted !== str check.
abdulazizalmalki-gh pushed a commit to abdulazizalmalki-gh/mouse-dictionary that referenced this pull request Oct 3, 2026
Syncs feat/arabic-packs with PR wtetsu#106 round-3 review fix: one run-based
regex (U+FF61-FF9F+), NFKC composes voiced marks within each run, no
separate NFC pass, no test()/replace() duplication.
@wtetsu

wtetsu commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Thanks!

@wtetsu
wtetsu merged commit 15a1bfa into wtetsu:master Oct 3, 2026
3 checks passed
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