compat: detect book language from text for invalid language code in metadata (#1743)

This commit is contained in:
Huang Xin
2025-08-04 20:55:18 +08:00
committed by GitHub
parent e3c05e7648
commit 3e5e4d2946
6 changed files with 48 additions and 10 deletions
@@ -105,7 +105,7 @@ const FoliateViewer: React.FC<{
bookKey,
viewSettings,
content: data,
transformers: ['punctuation', 'footnote'],
transformers: ['punctuation', 'footnote', 'language'],
};
return Promise.resolve(transformContent(ctx));
}
@@ -1,9 +1,11 @@
import type { Transformer } from './types';
import { footnoteTransformer } from './footnote';
import { languageTransformer } from './language';
import { punctuationTransformer } from './punctuation';
export const availableTransformers: Transformer[] = [
punctuationTransformer,
footnoteTransformer,
languageTransformer,
// Add more transformers here
];
@@ -0,0 +1,28 @@
import { detectLanguage, isValidLang } from '@/utils/lang';
import type { Transformer } from './types';
export const languageTransformer: Transformer = {
name: 'language',
transform: async (ctx) => {
let result = ctx.content;
const attrsMatch = result.match(/<html\b([^>]*)>/i);
if (attrsMatch) {
let attrs = attrsMatch[1] || '';
const langRegex = / lang="([^"]*)"/i;
const xmlLangRegex = / xml:lang="([^"]*)"/i;
const xmlLangMatch = attrs.match(xmlLangRegex);
const langMatch = attrs.match(langRegex);
if (!isValidLang(langMatch?.[1] || xmlLangMatch?.[1])) {
const mainContent = result.replace(/<[^>]+>/g, ' ');
const lang = detectLanguage(mainContent);
const newLangAttr = ` lang="${lang}"`;
const newXmlLangAttr = ` xml:lang="${lang}"`;
attrs = langMatch ? attrs.replace(langRegex, newLangAttr) : attrs + newLangAttr;
attrs = xmlLangMatch ? attrs.replace(xmlLangRegex, newXmlLangAttr) : attrs + newXmlLangAttr;
result = result.replace(attrsMatch[0], `<html${attrs}>`);
}
}
return result;
},
};
+4 -4
View File
@@ -138,6 +138,10 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
console.log('Loading book', key);
const { book: bookDoc } = await new DocumentLoader(file).open();
updateToc(bookDoc, config.viewSettings?.sortedTOC ?? false);
if (!bookDoc.metadata.title) {
bookDoc.metadata.title = getBaseFilename(file.name);
}
book.sourceTitle = formatTitle(bookDoc.metadata.title);
// Correct language codes mistakenly set with language names
if (typeof bookDoc.metadata?.language === 'string') {
if (bookDoc.metadata.language in SUPPORTED_LANGNAMES) {
@@ -145,10 +149,6 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
}
}
// Set the book's language for formerly imported books, newly imported books have this field set
if (!bookDoc.metadata.title) {
bookDoc.metadata.title = getBaseFilename(file.name);
}
book.sourceTitle = formatTitle(bookDoc.metadata.title);
const primaryLanguage = getPrimaryLanguage(bookDoc.metadata.language);
book.primaryLanguage = book.primaryLanguage ?? primaryLanguage;
book.metadata = book.metadata ?? bookDoc.metadata;
+9 -2
View File
@@ -1,9 +1,10 @@
import { EXTS } from '@/libs/document';
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
import { SUPPORTED_LANGS } from '@/services/constants';
import { getUserLang, isContentURI, isFileURI, isValidURL, makeSafeFilename } from './misc';
import { getStorageType } from './storage';
import { getDirFromLanguage } from './rtl';
import { SUPPORTED_LANGS } from '@/services/constants';
import { code6392to6391, isValidLang, normalizedLangCode } from './lang';
export const getDir = (book: Book) => {
return `${book.hash}`;
@@ -137,8 +138,14 @@ export const formatLanguage = (lang: string | string[] | undefined): string => {
: langCodeToLangName(lang || '');
};
// Should return valid ISO-639-1 language code, fallback to 'en' if not valid
export const getPrimaryLanguage = (lang: string | string[] | undefined) => {
return Array.isArray(lang) ? lang[0] : lang;
const primaryLang = Array.isArray(lang) ? lang[0] : lang;
if (isValidLang(primaryLang)) {
const normalizedLang = normalizedLangCode(primaryLang);
return code6392to6391(normalizedLang) || normalizedLang;
}
return 'en';
};
export const formatDate = (date: string | number | Date | null | undefined, isUTC = false) => {
+4 -3
View File
@@ -8,8 +8,8 @@ export const isCJKStr = (str: string) => {
export const isCJKLang = (lang: string | null | undefined): boolean => {
if (!lang) return false;
const normalizedLang = lang.split('-')[0]!.toLowerCase();
return ['zh', 'ja', 'ko'].includes(normalizedLang);
const normalizedLang = normalizedLangCode(lang);
return ['zh', 'ja', 'ko', 'zho', 'jpn', 'kor'].includes(normalizedLang);
};
export const normalizeToFullLang = (langCode: string): string => {
@@ -84,7 +84,8 @@ export const isSameLang = (lang1?: string | null, lang2?: string | null): boolea
export const isValidLang = (lang?: string) => {
if (!lang) return false;
const code = lang.split('-')[0]!.toLowerCase();
if (typeof lang !== 'string') return false;
const code = normalizedLangCode(lang);
return iso6392.some((l) => l.iso6391 === code || l.iso6392B === code);
};