From 1afeb897373237169f4744b480cfe1e159542b93 Mon Sep 17 00:00:00 2001 From: Eszter Date: Sat, 24 Oct 2026 19:08:45 +0200 Subject: [PATCH 1/2] Compare titles in every script when looking for duplicate books The duplicate check kept only a-z and 0-9, so two different Japanese titles that share a number (e.g. two Kumon workbooks for 1st grade) both became "1" and were offered for merging. --- app/src/Book.jsx | 3 ++- app/src/Library.test.jsx | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/Book.jsx b/app/src/Book.jsx index 605d2bd..a70c3f3 100644 --- a/app/src/Book.jsx +++ b/app/src/Book.jsx @@ -251,7 +251,8 @@ function Clippings({ session, book, revision = 0 }) { ) } -const norm = (s) => (s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() +// Letters and digits of every script: an ASCII-only pattern reduces Japanese titles to their digits. +const norm = (s) => (s ?? '').normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim() // Same normalized title (and author when both have one): probably the same book synced twice. export const looksLikeSame = (a, b) => a.document !== b.document && norm(a.title) && norm(a.title) === norm(b.title) && (!a.author || !b.author || norm(a.author) === norm(b.author)) diff --git a/app/src/Library.test.jsx b/app/src/Library.test.jsx index df60d78..9a5917f 100644 --- a/app/src/Library.test.jsx +++ b/app/src/Library.test.jsx @@ -46,3 +46,22 @@ describe('Library completion updates', () => { expect(queryByText('Continue reading')).toBeNull() }) }) + +describe('duplicate notice', () => { + const book = (document, title) => ({ document, title, author: 'Unknown Author', status: 'finished', percentage: 1 }) + + it('tells different Japanese titles apart', () => { + const books = [ + book('grammar', '小学1年生 言葉と文法にぐーんと強くなる (くもんの国語集中学習)'), + book('kanji', '小学1年生 漢字にぐーんと強くなる (くもんの国語集中学習)'), + ] + const { container } = render() + expect(container.textContent).not.toContain('appears more than once') + }) + + it('still finds a Japanese title synced twice, full-width forms included', () => { + const books = [book('a', '変な家2 ~11の間取り図~'), book('b', '変な家2 ~11の間取り図~')] + const { container } = render() + expect(container.textContent).toContain('appears more than once') + }) +}) From 846ae87dec103e0e4b3f26a5856396339ead7398 Mon Sep 17 00:00:00 2001 From: Eszter Date: Sat, 24 Oct 2026 19:31:20 +0200 Subject: [PATCH 2/2] Keep combining marks when comparing titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping marks made Thai titles that differ only in a tone mark look the same: ป่า (forest) and ป้า (aunt) both became "ป า". Hindi and Arabic vowel signs had the same problem. --- app/src/Book.jsx | 5 +++-- app/src/Library.test.jsx | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/Book.jsx b/app/src/Book.jsx index a70c3f3..d651e7a 100644 --- a/app/src/Book.jsx +++ b/app/src/Book.jsx @@ -251,8 +251,9 @@ function Clippings({ session, book, revision = 0 }) { ) } -// Letters and digits of every script: an ASCII-only pattern reduces Japanese titles to their digits. -const norm = (s) => (s ?? '').normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim() +// Letters, marks and digits of every script: an ASCII-only pattern reduces Japanese titles to their +// digits, and dropping marks merges Thai or Hindi words that differ only in a tone or vowel sign. +const norm = (s) => (s ?? '').normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, ' ').trim() // Same normalized title (and author when both have one): probably the same book synced twice. export const looksLikeSame = (a, b) => a.document !== b.document && norm(a.title) && norm(a.title) === norm(b.title) && (!a.author || !b.author || norm(a.author) === norm(b.author)) diff --git a/app/src/Library.test.jsx b/app/src/Library.test.jsx index 9a5917f..7c36f14 100644 --- a/app/src/Library.test.jsx +++ b/app/src/Library.test.jsx @@ -59,6 +59,12 @@ describe('duplicate notice', () => { expect(container.textContent).not.toContain('appears more than once') }) + it('tells Thai titles apart that differ only in a tone mark', () => { + const books = [book('forest', 'ป่า'), book('aunt', 'ป้า')] + const { container } = render() + expect(container.textContent).not.toContain('appears more than once') + }) + it('still finds a Japanese title synced twice, full-width forms included', () => { const books = [book('a', '変な家2 ~11の間取り図~'), book('b', '変な家2 ~11の間取り図~')] const { container } = render()