From 41fe42f29ba90f2c6c161cb5382c606bad525e4b Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:22:54 +0100 Subject: [PATCH] Fix "auto webtoon mode" detection The detection didn't work properly in case the selected manga was in a different language than English or the current active translation. In that case it didn't work because the translation for that locale wasn't loaded. So to ensure that the translation exists for each language, we have to hardcode it. --- .github/workflows/update_i18n_languages.yml | 1 + CHANGELOG.md | 1 + package.json | 1 + src/features/manga/Manga.constants.ts | 116 +++++++++++++++++- src/features/manga/services/Mangas.ts | 29 +---- .../reader/settings/ReaderSettings.utils.tsx | 4 +- tools/scripts/manga/generateMangaTypeTags.ts | 99 +++++++++++++++ 7 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 tools/scripts/manga/generateMangaTypeTags.ts diff --git a/.github/workflows/update_i18n_languages.yml b/.github/workflows/update_i18n_languages.yml index 85606821..eb12f3f1 100644 --- a/.github/workflows/update_i18n_languages.yml +++ b/.github/workflows/update_i18n_languages.yml @@ -48,6 +48,7 @@ jobs: cd master echo "WEBLATE_TOKEN=${{ secrets.MY_SECRET }}" > .env yarn i18n:gen-resources + yarn manga:gen-type-tags - name: Push changes run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index da9058f6..a2f98574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - (**Browse**) Fix missing pinned sources in the source language filter - (**Browse**) Fix incorrectly showing "local source" source in the source language filter (the local source can't be disabled) - (**Reader**) Fix page shift when toggling the "offset double spreads" setting (currently: enable: shift to the right; disable: shift to the left – now: inverted) +- (**Reader**) Fix "auto webtoon mode" detection for manga source languages other than english and the current selected language ## [20251230.01] (r2937) - 2025-12-30 diff --git a/package.json b/package.json index c8eb77e6..a6ae47ee 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dayjs:gen-locales-array": "tsx tools/scripts/dayjs/generateDayJsLocales.ts", "dayjs:gen-locales-import": "tsx tools/scripts/dayjs/generateDayJsLocalesImport.ts", "dayjs:gen-locales": "yarn dayjs:gen-locales-array && yarn dayjs:gen-locales-import", + "manga:gen-type-tags": "tsx tools/scripts/manga/generateMangaTypeTags.ts", "prepare": "husky" }, "engines": { diff --git a/src/features/manga/Manga.constants.ts b/src/features/manga/Manga.constants.ts index 07a58ff8..717fb59e 100644 --- a/src/features/manga/Manga.constants.ts +++ b/src/features/manga/Manga.constants.ts @@ -152,10 +152,124 @@ export const SOURCES_BY_MANGA_TYPE: Record = { ], }; -export const MANGA_TAGS_BY_MANGA_TYPE: Record = { +/** + * Lingui extraction markers — DO NOT remove. + * These `msg` calls ensure the strings stay in .po files for translation. + * The actual matching data is in {@link MANGA_TAGS_BY_MANGA_TYPE} below. + */ +// @ts-ignore - see comment +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const MANGA_TAG_DESCRIPTORS_BY_MANGA_TYPE: Record = { [MangaType.MANGA]: [msg`Manga`], [MangaType.COMIC]: [msg`Comic`], [MangaType.WEBTOON]: [msg`Webtoon`, msg`Long strip`], [MangaType.MANHWA]: [msg`Manhwa`, msg`Long strip`], [MangaType.MANHUA]: [msg`Manhua`, msg`Long strip`], }; + +/** + * All translations of manga type tags across all available locales. + * Used for matching manga genres to detect manga type regardless of source language. + * + * **IMPORTANT:** + * + * This is generated by the `manga:gen-type-tags` script + */ +export const MANGA_TAGS_BY_MANGA_TYPE: Record = { + [MangaType.MANGA]: ['Manga', 'مانگا', 'Mangá', 'Манга', 'மங்கா-', 'Truyện Nhật'], + [MangaType.COMIC]: [ + 'Comic', + 'کمیک', + 'Komik', + 'Fumetti', + 'アメコミ', + '만화', + 'Komiks', + 'Quadrinhos', + 'Комикс', + 'காமிக்', + 'Çizgi roman', + 'Truyện Hoa Kỳ', + '美漫', + ], + [MangaType.WEBTOON]: [ + 'Webtoon', + 'Long strip', + 'ويبتون', + 'وبتون', + 'ウェブトゥーン', + '웹툰', + 'Веб-комикс', + 'வெப்டூன்-', + 'Вебтун', + '条漫', + '條漫', + 'Tira larga', + 'نوار بلند', + 'Bande continue', + 'Függőleges folyamatos', + 'Strip panjang', + 'Striscia lunga', + '縦読み漫画', + '긴 스트립', + 'Długi pasek', + 'Tira longa', + 'Длинная полоска', + 'நீண்ட துண்டு', + 'Uzun şerit', + ], + [MangaType.MANHWA]: [ + 'Manhwa', + 'Long strip', + 'مانهوا', + '韓国マンガ', + '만화', + 'Манхва', + 'மன்அ்வா', + 'Truyện Hàn', + '韩漫', + '韓漫', + 'Tira larga', + 'نوار بلند', + 'Bande continue', + 'Függőleges folyamatos', + 'Strip panjang', + 'Striscia lunga', + '긴 스트립', + 'Długi pasek', + 'Tira longa', + 'Длинная полоска', + 'நீண்ட துண்டு', + 'Uzun şerit', + 'Cuộn dọc', + '条漫', + '條漫', + ], + [MangaType.MANHUA]: [ + 'Manhua', + 'Long strip', + 'مانها', + '漫画', + '만화', + 'Маньхуа', + 'மன்உவா', + 'Truyện Trung', + '國漫', + 'Tira larga', + 'نوار بلند', + 'Bande continue', + 'Függőleges folyamatos', + 'Strip panjang', + 'Striscia lunga', + '縦読み漫画', + '긴 스트립', + 'Długi pasek', + 'Tira longa', + 'Длинная полоска', + 'நீண்ட துண்டு', + 'Uzun şerit', + 'Cuộn dọc', + '条漫', + '條漫', + ], +}; diff --git a/src/features/manga/services/Mangas.ts b/src/features/manga/services/Mangas.ts index dfff9b64..c8ceb8e8 100644 --- a/src/features/manga/services/Mangas.ts +++ b/src/features/manga/services/Mangas.ts @@ -35,7 +35,6 @@ import { MangaGenreInfo, MangaIdInfo, MangaLocationState, - MangaSourceLngInfo, MangaSourceNameInfo, MangaThumbnailInfo, MangaTitleInfo, @@ -53,7 +52,6 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { assertIsDefined } from '@/base/Asserts.ts'; import { Confirmation } from '@/base/AppAwaitableComponent.ts'; import { UrlUtil } from '@/lib/UrlUtil.ts'; -import { DEFAULT_LANGUAGE } from '@/lib/ISOLanguageUtil.ts'; type MangaToMigrate = NonNullable; type MangaToMigrateTo = NonNullable['manga']; @@ -630,7 +628,7 @@ export class Mangas { } } - static getType(manga: MangaGenreInfo & MangaSourceNameInfo & MangaSourceLngInfo): MangaType { + static getType(manga: MangaGenreInfo & MangaSourceNameInfo): MangaType { if (Mangas.isType(manga, MangaType.MANGA)) { return MangaType.MANGA; } @@ -654,35 +652,16 @@ export class Mangas { return MangaType.MANGA; } - static isType(manga: MangaGenreInfo & MangaSourceNameInfo & MangaSourceLngInfo, type: MangaType): boolean { - const translateMangaTagsByMangaTypeEntries = Object.entries(MANGA_TAGS_BY_MANGA_TYPE).map( - ([mangaType, tags]) => [ - mangaType, - [DEFAULT_LANGUAGE, i18n.locale, manga.source?.lang] - .filter((lng) => !!lng) - .flatMap((language) => - tags.flatMap((tag) => - /* lingui-extract-ignore */ - i18n.t({ ...tag, values: { lng: language } }), - ), - ), - ], - ); - - const translatedMangaTagsByMangaType = Object.fromEntries(translateMangaTagsByMangaTypeEntries) as Record< - string, - string[] - >; - + static isType(manga: MangaGenreInfo & MangaSourceNameInfo, type: MangaType): boolean { const isMatchByGenre = manga.genre.some((genre) => - translatedMangaTagsByMangaType[type].some((tag) => genre.toLowerCase().includes(tag.toLowerCase())), + MANGA_TAGS_BY_MANGA_TYPE[type].some((tag) => genre.toLowerCase().includes(tag.toLowerCase())), ); const isMatchBySource = SOURCES_BY_MANGA_TYPE[type].includes(manga.source?.name.toLowerCase() ?? ''); return isMatchByGenre || isMatchBySource; } - static isLongStripType(manga: MangaGenreInfo & MangaSourceNameInfo & MangaSourceLngInfo): boolean { + static isLongStripType(manga: MangaGenreInfo & MangaSourceNameInfo): boolean { return ( Mangas.isType(manga, MangaType.WEBTOON) || Mangas.isType(manga, MangaType.MANHWA) || diff --git a/src/features/reader/settings/ReaderSettings.utils.tsx b/src/features/reader/settings/ReaderSettings.utils.tsx index 1b0971d5..fecbe885 100644 --- a/src/features/reader/settings/ReaderSettings.utils.tsx +++ b/src/features/reader/settings/ReaderSettings.utils.tsx @@ -16,7 +16,7 @@ import { ReaderPageScaleMode, ReadingMode, } from '@/features/reader/Reader.types.ts'; -import { MangaGenreInfo, MangaSourceLngInfo, MangaSourceNameInfo } from '@/features/manga/Manga.types.ts'; +import { MangaGenreInfo, MangaSourceNameInfo } from '@/features/manga/Manga.types.ts'; import { Mangas } from '@/features/manga/services/Mangas.ts'; import { ReaderPagedPager } from '@/features/reader/viewer/pager/components/ReaderPagedPager.tsx'; import { ReaderDoublePagedPager } from '@/features/reader/viewer/pager/components/ReaderDoublePagedPager.tsx'; @@ -57,7 +57,7 @@ export const isContinuousVerticalReadingMode = (readingMode: IReaderSettings['re [ReadingMode.CONTINUOUS_VERTICAL, ReadingMode.WEBTOON].includes(readingMode); export const isAutoWebtoonMode = ( - manga: MangaGenreInfo & MangaSourceNameInfo & MangaSourceLngInfo, + manga: MangaGenreInfo & MangaSourceNameInfo, shouldUseAutoWebtoonMode: IReaderSettings['shouldUseAutoWebtoonMode'], readingMode: IReaderSettingsWithDefaultFlag['readingMode'], ): boolean => shouldUseAutoWebtoonMode && readingMode.isDefault && Mangas.isLongStripType(manga); diff --git a/tools/scripts/manga/generateMangaTypeTags.ts b/tools/scripts/manga/generateMangaTypeTags.ts new file mode 100644 index 00000000..f6303485 --- /dev/null +++ b/tools/scripts/manga/generateMangaTypeTags.ts @@ -0,0 +1,99 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import path from 'path'; +import { readdirSync, readFileSync, writeFileSync } from 'fs'; +import { execSync } from 'child_process'; + +const OUTPUT_FILE_PATH = 'src/features/manga/Manga.constants.ts'; +const outputFilePath = path.join(import.meta.dirname, `../../../${OUTPUT_FILE_PATH}`); +const localesDirPath = path.join(import.meta.dirname, '../../../src/i18n/locales'); + +const MANGA_TYPE_TO_MSG_IDS = { + MANGA: ['Manga'], + COMIC: ['Comic'], + WEBTOON: ['Webtoon', 'Long strip'], + MANHWA: ['Manhwa', 'Long strip'], + MANHUA: ['Manhua', 'Long strip'], +} as const satisfies Record; + +type MangaTypeMsgId = (typeof MANGA_TYPE_TO_MSG_IDS)[keyof typeof MANGA_TYPE_TO_MSG_IDS][number]; + +const TARGET_MSG_IDS = new Set(Object.values(MANGA_TYPE_TO_MSG_IDS).flat()); + +const extractTranslations = (poContent: string): Map => + poContent.split('\n').reduce((translations, line, i, lines) => { + const msgIdMatch = line.trim().match(/^msgid "(.+)"$/); + if (!msgIdMatch) { + return translations; + } + + const msgId = msgIdMatch[1]; + if (!TARGET_MSG_IDS.has(msgId as MangaTypeMsgId)) { + return translations; + } + + const msgStrMatch = lines[i + 1]?.trim().match(/^msgstr "(.+)"$/); + if (!msgStrMatch) { + return translations; + } + + return new Map([...translations, [msgId, msgStrMatch[1]]]); + }, new Map()); + +const poFiles = readdirSync(localesDirPath).filter((file) => file.endsWith('.po')); + +const allTranslationsByMsgId = poFiles + .map((poFile) => extractTranslations(readFileSync(path.join(localesDirPath, poFile), 'utf-8'))) + .reduce( + (acc, translations) => + new Map( + [...acc].map(([msgId, existing]) => [ + msgId, + new Set([...existing, ...(translations.has(msgId) ? [translations.get(msgId)!] : [])]), + ]), + ), + new Map([...TARGET_MSG_IDS].map((msgId) => [msgId, new Set()])), + ); + +const translationsByMangaType = Object.fromEntries( + Object.entries(MANGA_TYPE_TO_MSG_IDS).map(([mangaType, msgIds]) => [ + mangaType, + [...new Set([...msgIds, ...msgIds.flatMap((msgId) => [...(allTranslationsByMsgId.get(msgId) ?? [])])])], + ]), +); + +const outputContent = readFileSync(outputFilePath, 'utf-8'); + +const mangaTypes = Object.keys(MANGA_TYPE_TO_MSG_IDS); +const entries = mangaTypes.map((type) => { + const tagList = translationsByMangaType[type].map((tag) => `'${tag.replace(/'/g, "\\'")}'`).join(','); + return ` [MangaType.${type}]: [\n${tagList},\n ]`; +}); + +const newBlock = `export const MANGA_TAGS_BY_MANGA_TYPE: Record = {\n${entries.join(',\n')},\n};`; + +const updatedContent = outputContent.replace( + /export const MANGA_TAGS_BY_MANGA_TYPE: Record = \{[\s\S]*?\n};/, + newBlock, +); + +writeFileSync(outputFilePath, updatedContent); + +console.log('Generated MANGA_TAGS_BY_MANGA_TYPE:'); +Object.entries(translationsByMangaType).forEach(([mangaType, tags]) => { + console.log(` ${mangaType}: ${tags.length} tags — ${tags.join(', ')}`); +}); + +const hasChanges = execSync('git status --porcelain').toString().includes(OUTPUT_FILE_PATH); + +if (hasChanges) { + execSync('git reset'); + execSync(`git add ${OUTPUT_FILE_PATH}`); + execSync('git commit -m "Update manga type tags"'); +}