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:
1
.github/workflows/update_i18n_languages.yml
vendored
1
.github/workflows/update_i18n_languages.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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',
|
||||
'条漫',
|
||||
'條漫',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
99
tools/scripts/manga/generateMangaTypeTags.ts
Normal file
99
tools/scripts/manga/generateMangaTypeTags.ts
Normal 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"');
|
||||
}
|
||||
Reference in New Issue
Block a user