From 1afeb897373237169f4744b480cfe1e159542b93 Mon Sep 17 00:00:00 2001 From: Eszter Date: Sat, 24 Oct 2026 19:08:45 +0200 Subject: [PATCH 1/3] 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/3] 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() From 3ae99392eae145765078be7d4b29f3e9150ced58 Mon Sep 17 00:00:00 2001 From: Eszter Date: Sat, 24 Oct 2026 19:50:49 +0200 Subject: [PATCH 3/3] Don't flag same-title books in different languages as duplicates Editions in two languages can share a title, like "1984" in English and in German. When both books have a language and the main subtags differ, they aren't offered for merging. A missing language still matches, and "en" matches "en-GB". --- app/src/Book.jsx | 11 +++++++++-- app/src/Library.test.jsx | 12 ++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/Book.jsx b/app/src/Book.jsx index d651e7a..4a55585 100644 --- a/app/src/Book.jsx +++ b/app/src/Book.jsx @@ -254,9 +254,16 @@ function Clippings({ session, book, revision = 0 }) { // 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. +// Language only by its main subtag ("en" and "en-GB" match). Unknown on either side counts as a match. +const sameLanguage = (a, b) => !a || !b || a.split('-')[0].toLowerCase() === b.split('-')[0].toLowerCase() +// Same normalized title (and author and language when both have one): probably the same book synced +// twice. Editions in two languages can share a title, e.g. "1984" in English and in German. 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)) + a.document !== b.document && + norm(a.title) && + norm(a.title) === norm(b.title) && + (!a.author || !b.author || norm(a.author) === norm(b.author)) && + sameLanguage(a.language, b.language) const field = 'h-11 w-full rounded-xl bg-surface px-3 text-base text-stone-900 ring-1 ring-stone-950/10 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-brand-500/60' diff --git a/app/src/Library.test.jsx b/app/src/Library.test.jsx index 7c36f14..9b86255 100644 --- a/app/src/Library.test.jsx +++ b/app/src/Library.test.jsx @@ -65,6 +65,18 @@ describe('duplicate notice', () => { expect(container.textContent).not.toContain('appears more than once') }) + it('tells the same title apart when the languages differ', () => { + const books = [{ ...book('en', '1984'), language: 'en' }, { ...book('de', '1984'), language: 'de' }] + const { container } = render() + expect(container.textContent).not.toContain('appears more than once') + }) + + it('treats a regional variant as the same language', () => { + const books = [{ ...book('a', '1984'), language: 'en' }, { ...book('b', '1984'), language: 'en-GB' }] + const { container } = render() + expect(container.textContent).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()