feat: support full book translation to bilingual books, closes #398 (#1240)

This commit is contained in:
Huang Xin
2025-05-26 18:23:33 +08:00
committed by GitHub
parent 033e161455
commit 699a01c78f
38 changed files with 576 additions and 64 deletions
+13
View File
@@ -26,3 +26,16 @@ export const langToDefaultLocale = (langCode: string): string => {
return mapping[langCode] || langCode;
};
export const localeToLang = (locale: string): string => {
const mapping: Record<string, string> = {
'zh-CN': 'zh-Hans',
'zh-TW': 'zh-Hant',
'zh-HK': 'zh-Hant',
};
if (locale.startsWith('zh')) {
return mapping[locale] || 'zh-Hans';
}
return mapping[locale] || locale.split('-')[0]!.toLowerCase();
};
+8
View File
@@ -40,6 +40,14 @@ export const getUserLang = () => {
return locale.split('-')[0] || 'en';
};
export const getTargetLang = () => {
const locale = getLocale();
if (locale.startsWith('zh')) {
return locale === 'zh-Hant' || locale === 'zh-HK' || locale === 'zh-TW' ? 'zh-Hant' : 'zh-Hans';
}
return locale.split('-')[0] || 'en';
};
export const isCJKEnv = () => {
const browserLanguage = navigator.language || '';
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
+12 -1
View File
@@ -353,6 +353,16 @@ export const getFootnoteStyles = () => `
}
`;
const getTranslationStyles = () => `
.translation-block-wrapper {
display: inline-block !important;
margin: 0.5em 0 !important;
}
.translation-target.hidden {
display: none !important;
}
`;
export interface ThemeCode {
bg: string;
fg: string;
@@ -432,8 +442,9 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
viewSettings.overrideFont!,
themeCode,
);
const translationStyles = getTranslationStyles();
const userStylesheet = viewSettings.userStylesheet!;
return `${layoutStyles}\n${fontStyles}\n${userStylesheet}`;
return `${layoutStyles}\n${fontStyles}\n${translationStyles}\n${userStylesheet}`;
};
export const mountAdditionalFonts = (document: Document) => {
+40
View File
@@ -0,0 +1,40 @@
export const walkTextNodes = (root: HTMLElement): HTMLElement[] => {
const elements: HTMLElement[] = [];
const walk = (node: HTMLElement | Document | ShadowRoot, depth = 0) => {
if (depth > 15) return;
if (node instanceof HTMLElement && node.shadowRoot) {
walk(node.shadowRoot, depth + 1);
}
const children = 'children' in node ? (Array.from(node.children) as HTMLElement[]) : [];
for (const child of children) {
if (child.tagName === 'STYLE' || child.tagName === 'LINK') {
continue;
}
if (child.shadowRoot) {
walk(child.shadowRoot, depth + 1);
}
if (child.tagName === 'IFRAME') {
const iframe = child as HTMLIFrameElement;
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc && iframeDoc.body) {
walk(iframeDoc.body, depth + 1);
}
}
const hasDirectText =
child.childNodes &&
Array.from(child.childNodes).some(
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),
);
if (child.children.length === 0 && child.textContent?.trim()) {
elements.push(child);
} else if (hasDirectText) {
elements.push(child);
} else if (child.children.length > 0) {
walk(child, depth + 1);
}
}
};
walk(root);
return elements;
};