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.
This commit is contained in:
schroda
2026-02-21 21:22:54 +01:00
parent 576cf85c38
commit 41fe42f29b
7 changed files with 223 additions and 28 deletions

View File

@@ -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: |

View File

@@ -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

View File

@@ -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": {

View File

@@ -152,10 +152,124 @@ export const SOURCES_BY_MANGA_TYPE: Record<MangaType, string[]> = {
],
};
export const MANGA_TAGS_BY_MANGA_TYPE: Record<MangaType, MessageDescriptor[]> = {
/**
* 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, MessageDescriptor[]> = {
[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, string[]> = {
[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',
'条漫',
'條漫',
],
};

View File

@@ -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<GetMangaToMigrateQuery['manga']>;
type MangaToMigrateTo = NonNullable<GetMangaToMigrateToFetchMutation['fetchManga']>['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) ||

View File

@@ -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);

View File

@@ -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<string, string[]>;
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<string, string> =>
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<string, string>());
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<string>()])),
);
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<MangaType, string[]> = {\n${entries.join(',\n')},\n};`;
const updatedContent = outputContent.replace(
/export const MANGA_TAGS_BY_MANGA_TYPE: Record<MangaType, string\[]> = \{[\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"');
}