嗨!看起来这是您首次登录 AO3 。如需了解如何使用 AO3 ,请查看一些新用户实用技巧,或浏览我们的常见问题解答。
如果您需要技术支持,请联系我们的支持团队;如果您遇到骚扰或对我们的服务条款(包括内容政策和隐私政策)有疑问,请联系我们的政策与滥用团队。
`; banner.innerHTML = translatedHTML; banner.setAttribute('data-translated-by-custom-function', 'true'); } /** * 专用翻译函数:翻译未登录时首页的介绍模块 */ function translateFrontPageIntro() { const introDiv = document.querySelector('div.intro.module.odd'); if (!introDiv || introDiv.hasAttribute('data-translated-by-custom-function')) { return; } const h2 = introDiv.querySelector('h2.heading'); if (h2) { h2.textContent = '一个由同人爱好者创建、由同人爱好者运营的非营利、非商业存档,收录再创作同人作品,如同人小说、同人画作、同人视频和同人有声作品'; } const statsP = introDiv.querySelector('p.stats'); if (statsP) { const counts = statsP.querySelectorAll('span.count'); if (counts.length === 3) { statsP.innerHTML = `超过 ${counts[0].textContent} 个同人圈 | ${counts[1].textContent} 名用户 | ${counts[2].textContent} 篇作品`; } } const parentP = introDiv.querySelector('p.parent'); if (parentP) { const link = parentP.querySelector('a'); if (link) { link.textContent = '再创作组织'; parentP.innerHTML = `Archive of Our Own 是隶属于${link.outerHTML}的一个项目。`; } } const accountDiv = introDiv.querySelector('div.account.module'); if (accountDiv) { const h4 = accountDiv.querySelector('h4.heading'); if (h4) { h4.textContent = '拥有 AO3 账户,您可以:'; } const listItems = accountDiv.querySelectorAll('ul li'); const translations = [ '分享您自己的同人作品', '在您喜欢的作品、系列或用户更新时收到通知', '参与各种活动', '记录您已浏览以及想要稍后查看的作品' ]; listItems.forEach((item, index) => { if (translations[index]) { item.textContent = translations[index]; } }); const paragraphs = accountDiv.querySelectorAll('p'); paragraphs.forEach(p => { if (p.textContent.includes('You can join by getting an invitation')) { p.textContent = '您可以通过我们的自动邀请队列获取邀请。所有同人爱好者和同人作品均受欢迎!'; } else if (p.classList.contains('actions')) { const inviteLink = p.querySelector('a'); if (inviteLink) { inviteLink.textContent = '获取邀请!'; } } }); } introDiv.setAttribute('data-translated-by-custom-function', 'true'); } /** * 专用翻译函数:翻译邀请请求页面 */ function translateInvitationRequestsPage() { function translateEnglishDate(englishDate) { const monthFullNameMap = { 'January': '1', 'February': '2', 'March': '3', 'April': '4', 'May': '5', 'June': '6', 'July': '7', 'August': '8', 'September': '9', 'October': '10', 'November': '11', 'December': '12' }; const dateParts = englishDate.trim().match(/(\w+)\s(\d{1,2}),\s(\d{4})/); if (dateParts && dateParts.length === 4) { const monthName = dateParts[1]; const day = dateParts[2]; const year = dateParts[3]; if (monthFullNameMap[monthName]) { const paddedDay = day.padStart(2, '0'); return `${year} 年 ${monthFullNameMap[monthName]} 月 ${paddedDay} 日`; } } return englishDate; } const mainDiv = document.querySelector('div#main[class*="invite_requests-"]'); if (!mainDiv) { return; } const isAlreadyHandled = mainDiv.hasAttribute('data-translated-by-custom-function'); const inviteStatusDiv = mainDiv.querySelector('#invite-status'); const statusHasContent = inviteStatusDiv && inviteStatusDiv.innerHTML.trim() !== ''; if (isAlreadyHandled && !statusHasContent) { return; } if (!isAlreadyHandled) { const h2 = mainDiv.querySelector('h2.heading'); if (h2) { const h2Text = h2.textContent.trim(); if (h2Text === 'Invitation Requests') { h2.textContent = '邀请请求'; } else if (h2Text === 'Invitation Request Status') { h2.textContent = '邀请请求状态'; } } const firstP = Array.from(mainDiv.querySelectorAll('p')).find(p => p.textContent.includes('To get a free Archive of Our Own account')); if (firstP) { const tosLink = firstP.querySelector('a[href="/tos"]'); const contentLink = firstP.querySelector('a[href="/content"]'); const privacyLink = firstP.querySelector('a[href="/privacy"]'); if (tosLink && contentLink && privacyLink) { tosLink.textContent = '服务条款'; contentLink.textContent = '内容政策'; privacyLink.textContent = '隐私政策'; firstP.innerHTML = `要获得免费的 AO3 账户,您需要一份邀请。将您的电子邮箱地址提交到我们的邀请队列,即表示您确认自己已年满 13 周岁;如果您所在国家或地区要求居民/公民需超过 13 周岁才能同意您的个人数据处理,您也已达到该年龄,无需我们获取母父或法定监护人的书面许可。我们仅会使用您提交的电子邮箱地址发送邀请,并处理/管理您的账户激活。请在阅读并同意我们的${tosLink.outerHTML}(包括${contentLink.outerHTML}和${privacyLink.outerHTML})后再申请邀请。`; } } const h3 = mainDiv.querySelector('h3.heading'); if (h3 && h3.textContent.trim() === 'Request an invitation') { h3.textContent = '申请邀请'; } const newRequestForm = mainDiv.querySelector('form#new_invite_request'); if (newRequestForm) { const label = newRequestForm.querySelector('label[for="invite_request_email"]'); if (label) { label.textContent = '电子邮箱'; } const submitButton = newRequestForm.querySelector('input[type="submit"]'); if (submitButton) { submitButton.value = '添加到列表'; } } const listInfoP = Array.from(mainDiv.querySelectorAll('p')).find(p => p.textContent.includes('check your position on the waiting list')); if (listInfoP) { const statusLink = listInfoP.querySelector('a'); if (statusLink) { statusLink.textContent = '查看自己在等待名单中的位置'; const originalText = listInfoP.textContent; const peopleCountMatch = originalText.match(/currently ([\d,]+) people/); const peopleCount = peopleCountMatch ? peopleCountMatch[1] : 'some'; const sendingCountMatch = originalText.match(/sending out ([\d,]+) invitations/); const sendingCount = sendingCountMatch ? sendingCountMatch[1] : 'some'; const hoursMatch = originalText.match(/every ([\d]+) hours/); const hours = hoursMatch ? hoursMatch[1] : 'some'; listInfoP.innerHTML = `如果您已提交邀请请求,可${statusLink.outerHTML}。目前等待名单上有 ${peopleCount} 人。我们每 ${hours} 小时发送 ${sendingCount} 份邀请。`; } } const statusP = Array.from(mainDiv.querySelectorAll('p')).find(p => p.textContent.includes('people on the waiting list')); if (statusP) { const originalText = statusP.textContent; const match = originalText.match(/There are currently ([\d,]+) people on the waiting list\.\s*We are sending out ([\d,]+) invitations every ([\d,]+) hours\./); if (match) { statusP.textContent = `当前等待名单上有 ${match[1]} 人。我们每 ${match[3]} 小时发送 ${match[2]} 个邀请。`; } } const statusForm = mainDiv.querySelector('form[action="/invite_requests/show"]'); if (statusForm) { const label = statusForm.querySelector('label[for="email"]'); if (label) { label.textContent = '电子邮箱'; } const submitButton = statusForm.querySelector('input[type="submit"][value="Look me up"]'); if (submitButton) { submitButton.value = '查找'; } } mainDiv.setAttribute('data-translated-by-custom-function', 'true'); } if (statusHasContent) { const statusH2 = inviteStatusDiv.querySelector('h2.heading'); if (statusH2 && !statusH2.hasAttribute('data-translated-by-custom-function')) { const match = statusH2.textContent.match(/Invitation Status for\s+(.+)/); if (match && match[1]) { statusH2.textContent = `${match[1].trim()} 的邀请状态`; statusH2.setAttribute('data-translated-by-custom-function', 'true'); } } const statusH3 = inviteStatusDiv.querySelector('h3.heading'); if (statusH3 && !statusH3.hasAttribute('data-translated-by-custom-function')) { const match = statusH3.textContent.match(/Invitation Status for\s+(.+)/); if (match && match[1]) { statusH3.textContent = `${match[1].trim()} 的邀请状态`; statusH3.setAttribute('data-translated-by-custom-function', 'true'); } } const paragraphs = inviteStatusDiv.querySelectorAll('p'); paragraphs.forEach(p => { if (p.hasAttribute('data-translated-by-custom-function')) return; const text = p.textContent.trim(); const sentMatch = text.match(/Your invitation was emailed to this address on\s+([\d-]+)\.\s*If you can't find it, please check your email spam folder as your spam filters may have placed it there\./); if (sentMatch) { p.textContent = `您的邀请已于 ${sentMatch[1]} 发送至此邮箱地址。如果您无法找到它,请检查您的垃圾邮件文件夹,因为它可能被误判为垃圾邮件。`; p.setAttribute('data-translated-by-custom-function', 'true'); return; } if (text.includes('Because your invitation was sent more than 24 hours ago, you can have your invitation resent.')) { p.textContent = '由于您的邀请已发送超过 24 小时,您可以申请重新发送。'; p.setAttribute('data-translated-by-custom-function', 'true'); return; } const resentMatch = text.match(/Your invitation was emailed to this address on\s+([\d-]+)\s+and resent on\s+([\d-]+)\.\s*If you can't find it/); if (resentMatch) { p.textContent = `您的邀请已于 ${resentMatch[1]} 发送至此邮箱地址,并于 ${resentMatch[2]} 重新发送。如果您无法找到它,请检查您的垃圾邮件文件夹,因为它可能被误判为垃圾邮件。`; p.setAttribute('data-translated-by-custom-function', 'true'); return; } if (text.includes('If it has been more than 24 hours since you should have received your invitation')) { p.textContent = '如果距离您应该收到邀请的时间已超过 24 小时,且检查垃圾邮件文件夹后仍未找到,您可以访问此页面重新发送邀请。'; p.setAttribute('data-translated-by-custom-function', 'true'); return; } }); const resendBtn = inviteStatusDiv.querySelector('form button[type="submit"]'); if (resendBtn && resendBtn.textContent.trim() === 'Resend Invitation') { resendBtn.textContent = '重新发送邀请'; } const statusResultP = inviteStatusDiv.querySelector('p'); if (statusResultP && !statusResultP.hasAttribute('data-translated-by-custom-function')) { const match = statusResultP.innerHTML.match(/You are currently number ([\d,]+)<\/strong> on our waiting list!\s*At our current rate, you should receive an invitation on or around:\s*(.+)\./s); if (match) { const englishDate = match[2].trim(); const translatedDate = translateEnglishDate(englishDate); statusResultP.innerHTML = `您目前在等待名单上的位置是第 ${match[1]} 位!按照当前速度,您应在 ${translatedDate} 前后收到邀请。`; statusResultP.setAttribute('data-translated-by-custom-function', 'true'); } else if (/Sorry, we can't find the email address you entered/.test(statusResultP.textContent)) { statusResultP.textContent = '抱歉,我们无法找到您输入的电子邮箱地址。'; statusResultP.setAttribute('data-translated-by-custom-function', 'true'); } } } const successNotice = mainDiv.querySelector('div.flash.notice'); if (successNotice && successNotice.textContent.includes("You've been added to our queue!")) { const match = successNotice.innerHTML.match(/around (.+?)\. We strongly recommend/); if (match && match[1]) { const englishDate = match[1]; const translatedDate = translateEnglishDate(englishDate); successNotice.innerHTML = `您已进入排队列表!我们预计您将在 ${translatedDate} 前后收到邀请。我们强烈建议您将 do-not-reply@archiveofourown.org 添加到您的通讯录,以防邀请邮件被您的邮件服务商误判为垃圾邮件。`; } } const errorDiv = mainDiv.querySelector('div#error.error'); if (errorDiv) { const errorH4 = errorDiv.querySelector('h4'); if (errorH4) { errorH4.textContent = '抱歉!我们无法保存此邀请请求,因为:'; } const errorMessages = { "Email can't be blank": "电子邮箱 不能为空。", "Email should look like an email address.": "电子邮箱 格式不正确。", "Email is already being used by an account holder.": "该电子邮箱地址已被其她账户使用。", }; const errorLis = errorDiv.querySelectorAll('ul li'); errorLis.forEach(li => { const originalError = li.textContent.trim(); if (errorMessages[originalError]) { li.textContent = errorMessages[originalError]; } }); } } /** * 专用翻译函数:翻译“请求过于频繁”的错误页面 */ function translateTooManyRequestsPage() { const body = document.body; if (body.hasAttribute('data-translated-by-custom-function')) { return; } document.title = "请求过于频繁! | AO3 作品库"; const headerH1 = document.querySelector('header h1'); if (headerH1) { headerH1.innerHTML = 'AO3 作品库 beta'; } const mainH2 = document.querySelector('main h2'); if (mainH2) { const logoImg = mainH2.querySelector('img.logo'); if (logoImg) { mainH2.innerHTML = `${logoImg.outerHTML} 请求页面过于频繁。`; } } const paragraphs = document.querySelectorAll('main p'); if (paragraphs.length >= 2) { paragraphs[0].textContent = '我们已阻止此操作以保护系统安全。请一次加载较少页面或放慢浏览速度,并在几分钟后重试。'; const supportLink = paragraphs[1].querySelector('a'); if (supportLink) { supportLink.textContent = '联系支持团队'; paragraphs[1].innerHTML = `如果问题依旧存在,请 ${supportLink.outerHTML} 。`; } } const footerSmall = document.querySelector('footer small'); if (footerSmall) { const bTags = footerSmall.querySelectorAll('b'); if (bTags.length === 2) { bTags[0].textContent = 'Ray ID:'; bTags[1].textContent = '您的 IP:'; } const showIpLink = footerSmall.querySelector('#client-ip-reveal'); if (showIpLink) { showIpLink.textContent = '显示 IP'; } } body.setAttribute('data-translated-by-custom-function', 'true'); } /** * 专门用于翻译 /works/search 页面上的“作品搜索”帮助文本。 */ function translateWorkSearchTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work search text help') { return; } if (container) { container.innerHTML = `搜索数据库中与作品相关的所有字段,包括简介、注释和标签,但不包括作品全文。
字符“:”和“@”具有特殊含义。请不要在搜索中使用它们,否则会得到意想不到的结果。就像在“标题”和“作者/画师”字段中,您可以使用以下运算符来组合搜索词:
创建一个时间范围。如果未提供范围,将根据指定的时间段自动计算。
可用时间段:year, month, week, day, hour(年、月、周、天、小时)
示例(以 2012 年 4 月 25 日 星期三 为当前日期):
请注意,“ago”(之前/前)一词是可选的。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品搜索:日期 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /works/search 页面上“跨圈作品”相关的帮助文本框。 */ function translateWorkSearchCrossoverTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work search crossover help') { return; } if (container) { container.innerHTML = `一般来说,跨圈作品指包含多个同人圈的作品。在筛选时,如果一篇作品被标注为两个或更多 不相关的 同人圈,就被视为跨圈作品(我们使用标签整理系统来做出此判定)。
想要查找两个特定同人圈之间的跨圈作品?请在搜索表单中的“同人圈”字段输入它们的名称,或在筛选器中选择/输入这两个同人圈。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '搜索:跨圈作品 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /works/search 页面上“数值”相关的帮助文本框。 */ function translateWorkSearchNumericalTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work search numerical help') { return; } if (container) { container.innerHTML = `在查找具有特定字数、点击量、点赞数、评论或书签数量的作品时,请使用以下指南。注意句号和逗号会被忽略:1.000 = 1,000 = 1000。
从此下拉菜单中选择一种语言即可搜索该语言的作品。请注意,此列表包含我们当前支持的所有语言,并非所有选项都能返回结果。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品搜索:语言 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /works/search 页面上“标签”相关的帮助文本框。 */ function translateWorkSearchTagsTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work search tags help') { return; } if (container) { container.innerHTML = `“同人圈”、“角色”、“关系”以及“附加标签”字段在输入搜索词时会提供标签建议。选择“规范”或常用标签(即自动补全列表中出现的标签)将匹配出所有包含该标签、本标签的同义标签以及与之关联的子标签的结果。例如,选中规范关系标签 Erika Mustermann/Juan Pérez 后,系统也会匹配出被标注为 Juan Pérez/Erika Mustermann 的作品,前提是这些标签已由标签管理员在后台关联完成。更多信息可参阅“什么是‘规范’标签?”。
如果某个标签未出现在自动补全列表中,并不代表该标签在 Archive 中不存在;它可能仅尚未被标签管理员标记为常用标签。您可以在此字段输入任意词语或短语。如果您的短语未精确匹配某个常用标签,搜索则会检索所有包含该短语中词语的标签。例如,输入 People Doing Things 同时也会匹配 Nice People Doing Things、People Doing Shady Things 和 People Doing Things with Spoons 等标签。但在这种情况下,搜索结果可能会比较不可预测。
输入的搜索词越多、选项越多,搜索结果就越精确。默认情况下,所有搜索条件之间是 AND 关系:输入两个同人圈时,只会匹配出同时包含这两个同人圈标签的作品,而不是两个同人圈任意一个的所有作品;同理,输入两个角色时,只会匹配出同时包含这两个角色的作品;同时选中 女/男 和 男/男 关系标签,则仅会匹配出同时包含这两种关系标签的作品,依此类推。
更多关于标签的内容请参阅我们的标签常见问题(Tags FAQ),更多关于标签搜索的说明请参阅搜索与浏览常见问题(Search and Browse FAQ)。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品搜索:标签 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /people/search 页面上“用户搜索”相关的帮助文本框。 */ function translatePeopleSearchTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'People search all fields') { return; } if (container) { container.innerHTML = `在“搜索所有字段”中输入文本,以查找用户名、笔名或笔名描述中包含搜索词的用户。
字符“:”和“@”具有特殊含义。请不要在搜索中使用它们,否则会得到意想不到的结果。
使用以下指南输入搜索词和搜索运算符。“任意字段”会组合搜索表单中的所有文本字段(包括标签)。“书签创建者”可让您搜索由特定用户创建的书签。“注释”会在所有书签创建者的注释中搜索词条。
字符“:”和“@”具有特殊含义。请不要在搜索中使用它们,否则会得到意想不到的结果。
“作品标签”字段会搜索条目创建者为已创建书签作品添加的所有标签,不包括书签创建者自己添加的标签。标签类型可为:分级、预警、分类、同人圈、角色、关系、附加标签。该字段在您输入搜索关键词时会建议规范标签。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:作品标签 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“类型”相关的帮助文本框。 */ function translateBookmarkSearchTypeTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark search type help') { return; } if (container) { container.innerHTML = `选择已创建书签条目的类型,以将搜索结果限制为“作品”、“系列”或“外部作品”。请注意,选择“外部作品”时,将匹配出所有托管于 Archive 之外的作品的书签。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:类型 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“更新日期”相关的帮助文本框。 */ function translateBookmarkSearchDateUpdatedTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark search date updated help') { return; } if (container) { container.innerHTML = `指定一个时间范围,以查找在该时间段内发布或更新的已创建书签条目,例如有新章节的作品或有新作品的系列。如果作品被创作者自定义发布日期(即上传时设置了与实际上传日期不同的发布日期),则该自定义发布日期将用于本次搜索。
您可以按 year, month, week, day, hour(年、月、周、天或小时)进行搜索。
“ago”(之前/前)一词是可选的。请注意,“ 1 天前”并不是一个范围,只会查找恰好在昨天此时更新的条目。如有需要,应当创建一个区间来搜索。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:更新日期 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“书签创建者的标签”相关的帮助文本框。 */ function translateBookmarkSearchBookmarkerTagsTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark search bookmarker tag') { return; } if (container) { container.innerHTML = `“书签创建者的标签”字段会搜索书签创建者为该书签添加的所有标签,不包括作品或系列本身的标签。标签类型可为:分级、预警、分类、同人圈、角色、关系、附加标签。该字段在您输入搜索词时会建议规范标签。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:书签创建者标签 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“推荐”相关的帮助文本框。 */ function translateBookmarkSearchRecTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark search rec help') { return; } if (container) { container.innerHTML = `选择此选项可将搜索范围限定为书签创建者标记为“推荐”的书签。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:推荐 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“含注释”相关的帮助文本框。 */ function translateBookmarkSearchNotesTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark search notes help') { return; } if (container) { container.innerHTML = `选择此选项可将搜索范围限定为带有书签创建者添加注释的书签。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:注释 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“添加日期”相关的帮助文本框。 */ function translateBookmarkSearchDateBookmarkedTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark search date bookmarked help') { return; } if (container) { container.innerHTML = `指定一个时间范围,以查找在该时间段内创建的书签。这可能与已创建书签条目的发布或更新时间不同。
您可以按 year, month, week, day, hour(年、月、周、天或小时)进行搜索。
“ago”(之前/前)一词是可选的。请注意,“ 1 天前”并不是一个范围,只会查找恰好在昨天此时创建的书签。如有需要,应当创建一个区间来搜索。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签搜索:添加日期 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /tags/search 页面上“文本搜索”相关的帮助文本框。 */ function translateTagSearchTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Tag search text help') { return; } if (container) { container.innerHTML = `富文本编辑器(RTE)的具体行为取决于您的设备、浏览器、操作系统以及您粘贴内容的来源。但是,从一个格式规范的文档开始,将有助于您最大程度地利用 RTE 。以下是一些通用技巧,以确保您的格式尽可能被保留:
在段落之间按一次 Enter 键。按两次 Enter 会插入一个空段落,当您粘贴到 RTE 时,会在段落之间产生额外的、可能不需要的空格。Archive 使用顶部和底部边距来制造段落间的空行效果;您可以使用文本编辑器中的段落格式选项来达到类似效果,而无需添加额外的 <p> 标签。
为标题、块引用、代码等使用预设样式。通常在文本编辑器的 “格式” 菜单中找到的 “样式” 选项,在粘贴到 RTE 时通常会转换为 HTML 标签。仅仅通过改变字体大小、字体名称或文本缩进来模拟标题或块引用的视觉效果,通常是不会起作用的。
Google Drive 使用内联 CSS 来改变文本对齐方式以及产生粗体、斜体、下划线和删除线格式。遗憾的是,我们不允许在 Archive 上使用内联样式,因此只有纯 HTML 格式(如标题、列表、链接和表格)会被保留。
在某些浏览器中,格式在粘贴到 RTE 时可能看起来被保留了,但在预览或发布您的作品时,它将被我们的 HTML 清理程序移除。
Scrivener 用户通常通过粘贴到 HTML 编辑器,然后切换到 RTE 进行修改,可以获得更好的效果。要从 Scrivener 复制 HTML,请执行以下操作:
下划线和删除线通常由 CSS 产生。因为 Archive 不允许使用内联 CSS,这些文本样式在粘贴时经常会丢失。
从使用 <u>、<del>、<strike> 或 <s> 标签的网页粘贴将可以正常工作。
文本对齐现在通常通过 CSS 实现,并且因为 Archive 不允许内联 CSS,对齐方式在粘贴时通常会丢失。
从使用 align 属性和 <center> 元素的来源粘贴将保持格式完整,但请注意,RTE 中的对齐按钮无法修改用 <center> 标签创建的居中对齐。
文本编辑器为其标题预设使用许多不同的样式。例如,在 OpenOffice 中选择 “标题 4” 会产生斜体的无衬线文本。即使成功将标题粘贴到 RTE 中,这种视觉格式也不会被保留——只有 <h4> 标签会被保留。这不是 bug 。HTML 旨在告诉浏览器文本的含义(例如:“这是一个标题” ),而不是它应该如何显示(例如:“这应该是 Arial 字体” )。如果您希望修改标题或作品任何其她部分的样式,请使用作品界面。
缩进文本是一种纯粹的视觉效果,没有等效的 HTML,并且不会被保留。请使用作品界面来缩进文本。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '富文本 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用翻译函数:翻译“HTML帮助”弹窗 */ function translateHtmlHelpModal() { const modal = document.querySelector('#modal'); if (!modal) return; const h2 = modal.querySelector('h2'); if (!h2 || !h2.textContent.includes('HTML on the Archive')) { return; } const contentDiv = modal.querySelector('.content.userstuff'); if (!contentDiv) return; contentDiv.innerHTML = `
a, abbr, acronym, address, [align], [alt], [axis], b, big, blockquote, br, caption, center, cite, [class], code, col, colgroup, dd, del, details, dfn, div, dl, dt, em, figcaption, figure, h1, h2, h3, h4, h5, h6, [height], hr, [href], i, img, ins, kbd, li, [name], ol, p, pre, q, rp, rt, ruby, s, samp, small, span, [src], strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, [title], tr, tt, u, ul, var, [width]
当您在 Archive 上输入 HTML 时,我们会对其进行清理,以确保安全(防止垃圾邮件发送者和黑客上传恶意内容),并为方便您和提高可访问性做一些基本格式化。我们采取的格式化步骤如下:
当您第一次输入 HTML 后再次编辑时,您将看到我们格式化的结果,以便纠正任何我们的格式化程序可能造成的错误。请注意,获得良好效果的最佳方式是输入规范的 HTML——这样您的作品在各浏览器、屏幕阅读器、移动设备和下载时都会正确显示。
“良好 HTML” 意味着能准确标注文本含义的 HTML——如果是段落,应使用段落标签,而不仅仅是使用换行标签分隔。如果是强调文字,应使用 <em> 标签。如果是项目列表,每个项目都应放在列表标签内。如果<em>不是</em>一个项目列表,您就不应该使用列表标签。:)
如果您发现自己为了达到某种视觉效果而输入了不符合语义的 HTML,请尽量避免!“作品界面”功能允许您对作品应用自定义 CSS,让它们呈现您想要的任何样式(前提是从“良好 HTML”开始会更容易)。
一些具体建议:
h1、h2、h3、h4、h5、h6em、strong<em>Rodney</em>Mckay
我 <strong>永远都不会</strong>理解你!
blockquote、q、cite</blockquote>引用一段文字
使用 q 来<q>引用短句
</q>
使用 cite 来引用<cite>书名或文章名</cite>
如果您曾想在 Archive 举办挑战活动,就可以使用标签集。您可以创建一个标签集,列出所有应当出现在报名表单中的标签,即使这些标签此前在 Archive 上尚未使用过,然后将此标签集添加到您的挑战活动中。报名表单将自动显示标签集中包含的标签。
您可以添加任意数量的管理员协助管理标签集(无需开放活动设置权限),还可以允许活动参与者提名要添加到标签集的新标签。您及管理员可审核这些提名,并选择批准或拒绝。您可以为标签集中的新标签添加同人圈关联,或交由标签管理员处理(这可能需要一些时间)。
所有标签集均展示于“标签集主页面”,浏览它们有助于您更深入理解其运作机制。
部分用户可能会选择将自己的标签集公开共享,供她人在活动中使用。请注意,标签集的所有者可以随时删除或修改标签集而不另行通知,因此在使用她人标签集举办挑战活动前,请务必确认该标签集所有者不会对其进行变更。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '关于标签集'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门翻译“标签提名”页面的提名规则说明。 * @param {string} originalText - 匹配到的原始英文句子。 * @returns {string} - 动态构建的中文翻译。 */ function translateNominationRule(originalText) { const componentRules = { fandoms: { regex: /([\d,]+) fandoms/, template: "$1 个同人圈" }, characters: { regex: /([\d,]+) characters/, template: "$1 个角色" }, relationships: { regex: /([\d,]+) relationships/, template: "$1 对关系" }, additionalTags: { regex: /([\d,]+) additional tags/, template: "$1 个附加标签" } }; const parts = {}; for (const key in componentRules) { const match = originalText.match(componentRules[key].regex); if (match) { parts[key] = match[1]; } } if (Object.keys(parts).length === 0) { return originalText; } const mainClauses = []; const perFandomClauses = []; let additionalTagClause = ''; const hasFandomContext = !!parts.fandoms || originalText.includes('for each one'); if (parts.fandoms) { mainClauses.push(componentRules.fandoms.template.replace('$1', parts.fandoms)); } if (!hasFandomContext) { if (parts.characters) mainClauses.push(componentRules.characters.template.replace('$1', parts.characters)); if (parts.relationships) mainClauses.push(componentRules.relationships.template.replace('$1', parts.relationships)); if (parts.additionalTags && !originalText.includes('You can also nominate')) { mainClauses.push(componentRules.additionalTags.template.replace('$1', parts.additionalTags)); } } if (hasFandomContext) { if (parts.characters) perFandomClauses.push(componentRules.characters.template.replace('$1', parts.characters)); if (parts.relationships) perFandomClauses.push(componentRules.relationships.template.replace('$1', parts.relationships)); } if (parts.additionalTags && originalText.includes('You can also nominate')) { additionalTagClause = ` 您也可以最多提名 ${componentRules.additionalTags.template.replace('$1', parts.additionalTags)}。`; } let finalTranslation = ''; if (mainClauses.length > 0) { finalTranslation = `您最多可提名 ${mainClauses.join('和 ')}`; } if (perFandomClauses.length > 0) { if (finalTranslation === '') { finalTranslation = `您最多可为每个同人圈提名 ${perFandomClauses.join('和 ')}。`; } else { finalTranslation += `,最多可为每个同人圈提名 ${perFandomClauses.join('和 ')}。`; } } else if (finalTranslation !== '') { finalTranslation += '。'; } finalTranslation += additionalTagClause; return finalTranslation.trim() || originalText; } /** * 专用翻译函数:翻译“预警”相关的帮助文本框 */ function translateWarningHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Warning help') { return; } if (container) { container.innerHTML = `由于法律及其她原因,AO3 要求用户必须为一组常见预警(血腥暴力描写、主要角色死亡、强暴/非自愿性行为、未成年性行为)选择是否预警。创作者可在此框架内选择不预警其中某些内容,或添加额外预警。
您还可以使用“附加标签”字段添加其她或更详细的预警。有关预警的政策请参阅服务条款及服务条款常见问题。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '预警 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用翻译函数:翻译“同人圈”相关的帮助文本框 */ function translateFandomHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Fandom help') { return; } if (container) { container.innerHTML = `您的作品所属的同人圈名称。请使用全称,而非缩写。您可以列出多个同人圈,使用逗号分隔(例如,您的作品是跨圈同人文)。
要了解有关标签的更多信息,包括如何添加 Archive 上尚不存在的标签,请参阅我们的标签常见问题解答。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '同人圈 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用翻译函数:翻译“书签图标”说明弹窗 */ function translateBookmarkSymbolsKeyModal() { const footerTitle = document.querySelector('#modal div.footer span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark symbols key') { return; } const modal = footerTitle.closest('#modal'); if (!modal) { return; } const mainTitle = modal.querySelector('div.content.userstuff > h4'); if (mainTitle) { mainTitle.textContent = '书签图标'; } const definitions = modal.querySelectorAll('#bookmark-symbols-key > dd'); const translations = [ '推荐', '公开书签', '私人书签', '此书签已被管理员隐藏' ]; if (definitions.length === translations.length) { definitions.forEach((dd, index) => { dd.textContent = translations[index]; }); } footerTitle.textContent = '书签图标说明'; const closeButton = modal.querySelector('div.footer a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } modal.setAttribute('data-translated-by-custom-function', 'true'); } /** * 专用翻译函数:翻译“分级”相关的帮助文本框 */ function translateRatingHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Rating help') { return; } if (container) { container.innerHTML = `(要了解更多信息,请参阅 AO3 服务条款的分级与预警部分。)
(要了解更多信息,请参阅 Archive 标签常见问题。)
Archive 上的作品分为 6 类。以下为各缩写含义,具体定义因同人圈和用户而异;请选择最适用的分类,或留空:
(要了解更多信息,请参阅 Archive 标签常见问题。)
对于您作品中存在的关系,请尽可能使用全名(例如"Mickey Mouse/Minnie Mouse"或"Rodney McKay & John Sheppard"),可通过逗号分隔列出多个关系。请注意,所有用户创建的标签均不得超过 150 字符;若作品包含大型多角关系或名称较长的多名角色,建议将名称缩短为仅有名字或带首字母的姓氏,以避免超过字符限制且保持可识别性。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '关系 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译角色标签帮助弹窗。 */ function translateCharactersHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Characters help') { return; } if (container) { container.innerHTML = `(要了解更多信息,请参阅 Archive 标签常见问题。)
您作品中的主要角色,请使用全名并以逗号分隔。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '角色 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Additional Tags"帮助弹窗的文本。 */ function translateAdditionalTagsHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Additional tags help') { return; } if (container) { container.innerHTML = `(要了解更多信息,请参阅 Archive 标签常见问题。)
您希望为作品添加的其她标签(例如:"虐心"、"跨圈"或"触手")。您也可以用此字段来标注 Archive 预警中未涵盖的内容。请不要在此填写同人圈、关系或角色名称。多个标签请用逗号分隔。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '附加标签 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Adding To Collections"帮助弹窗的文本。 */ function translateCollectionsHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Add collectible to collection') { return; } if (container) { container.innerHTML = `以逗号分隔输入合集名称,您正在编辑的作品将被添加到您指定的所有合集中。
请注意,您需要使用合集的名称(用于生成合集网址),而非其展示标题(因为不同合集允许重名)。合集名称与您的用户登录名相同。若启用 JavaScript ,名称会自动补全。
另请注意,如果您提交的合集受管理员审核,且您不是成员,您的作品不会自动添加——必须等待管理员批准后才会加入。如果这是匿名和/或未公开的合集,则作品发布后立即以匿名和/或隐藏状态展示,包括在等待审核期间。若作品被拒,则会保持匿名和/或未公开状态,直到您将其从合集中移除或管理员取消关联。
如果您改变主意想将作品从合集中移除,可在编辑时修改合集列表,或在账户的"我的合集"页面管理所有已创建书签作品。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '将作品添加到合集'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Recipients"帮助弹窗的文本。 */ function translateRecipientsHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Recipients') { return; } if (container) { container.innerHTML = `请输入赠文对象的名称,以逗号分隔!
如果您的作品是送给某人的礼物或为她们而作,您可以在此输入她们的姓名,作品署名下方会显示这些信息。 赠文对象无需是 Archive 的注册用户,但如果有匹配的笔名,自动补全会提供建议。我们会通知被选为赠文对象的注册用户。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '赠文对象'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Parent Works Help"帮助弹窗的文本。 */ function translateParentWorksHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Parent works help') { return; } if (container) { container.innerHTML = `如果您正在创建新作品,目前只能添加一个灵感来源。 若要添加更多,请先保存您的作品,然后在已发布作品页面点击"编辑"按钮,再像之前那样添加新的灵感来源。
您添加为灵感来源的所有作品将显示在此表单下方,标题为"当前母作品"。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '母作品 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Choosing Series"帮助弹窗的文本。 */ function translateChoosingSeriesHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Choosing series') { return; } if (container) { container.innerHTML = `系列是一组相关的故事,每个故事独立完整。 您可以随时在个人中心中创建新系列或将作品添加到系列中。
如果您想发布正在创作中的作品或分章节故事,请选择多章节作品功能。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '选择系列'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Publication Date Options"帮助弹窗的文本。 */ function translateBackdatingHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Backdating help') { return; } if (container) { container.innerHTML = `发布作品时,您可以选择设置不同的发布日期——也就是为作品回溯日期。您也可以为各单独章节设置发布日期。请注意,这两种情况下都会影响作品在个人中心和作品页面中的显示顺序和位置。这些页面显示的更新日期,将取您作品或任一章节的发布日期,以较晚者为准。
您添加的后续章节将在表单中预填此日期,您仍可手动覆盖该日期。这意味着如果您不清楚或不在意章节的实际发布日期,也能方便地为作品回溯日期。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '回溯日期 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译"Languages"帮助弹窗的文本。 */ function translateLanguagesHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Languages help') { return; } if (container) { container.innerHTML = `列表中没有您的语言?请通过支持表单告诉我们,我们会很高兴将其添加!(请放心,您现在可以发布作品,并在稍后更改语言。)外部作品无需选择语言。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '语言 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 Work Skins 弹窗页面的帮助文本。 */ function translateWorkSkins() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work skins') { return; } if (container) { container.innerHTML = `您可以像为 Archive 创建界面一样,为您的作品创建自定义样式表或"界面"。主要区别在于,作品界面会改变其她用户查看作品的方式,而不仅仅是您自己看到的效果。
作品界面仅影响所应用作品的正文——无法通过它们更改 Archive 的导航或背景。不过,您可以创建自定义类。例如,您可以更改部分文字的颜色,对某些段落进行特定方式的缩进,等等。
例如,假设您希望将文中某个单词设置为亮蓝色,可以按以下步骤操作:
.bluetext {color: blue;}
I want <span class="bluetext">house</span> to be in blue
要了解更多信息,请参阅教程:创建作品界面和界面与 Archive 界面常见问题。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品界面'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 Registered Users 弹窗页面的帮助文本。 */ function translateRegisteredUsers() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Registered users') { return; } if (container) { container.innerHTML = `注册用户是拥有 Archive 账号的用户。勾选此选项后,您的作品仅限已登录用户查看。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '注册用户'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 Comments Moderated 弹窗页面的帮助文本。 */ function translateCommentsModerated() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Comments moderated') { return; } if (container) { container.innerHTML = `启用此功能后,您必须审核并批准所有评论,评论才会在作品上公开显示。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '评论需审核'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 Who can comment on this work 弹窗页面的帮助文本。 */ function translateWhoCanComment() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Who can comment on this work') { return; } if (container) { container.innerHTML = `更改设置不会影响现有评论。如需删除已有评论,请参阅我能编辑或删除她人留下的评论吗?以了解详情。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '谁可以评论此作品'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译导入疑难解答弹窗的帮助文本。 */ function translateWorkImportTroubleshooting() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work import') { return; } if (container) { container.innerHTML = `如果您的文本在出现破折号或带重音字符处被截断,您可能需要使用下方的"设置自定义编码"菜单手动设置编码,才能成功导入作品。有效的编码类型可能有所不同;您可能需要尝试多个选项以找到正确的编码。有关更多信息,请参阅编码帮助页面。
如果您要从 e-fiction 网站导入带章节的作品,您需要分别输入每一章的 URL ,每行一个。一次最多可以导入 200 个章节。有关从其她网站导入作品的更多信息,请参阅"我如何从其她网站导入作品?"
如果您想将已发布在 AO3 上的作品从一个用户账户转移到另一个账户,您必须编辑现有作品,将新账户添加为共同创作者,然后移除旧账户。不能使用导入工具处理 AO3 上托管的作品。
除非您勾选"覆盖标签和说明"选项框,否则您在"标签"下输入的信息仅在导入工具无法从作品中识别标签时才会使用。
导入完成后,您将可以编辑并完善标准的作品信息。有关发布和编辑的更多信息,请参阅发布与编辑常见问题。
如果上述信息都无法解决您的问题,您也许可以在已知问题页面中找到答案。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品导入'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译编码帮助弹窗的帮助文本。 */ function translateEncodingHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Encoding help') { return; } if (container) { container.innerHTML = `如果导入工具剥离了作品中的特殊字符(例如变音符号或弯引号),或未能导入整段文本,可能是由于自动检测您作品编码时出现了问题。UTF-8 是常见的编码,但也有其她编码需要您手动指定,以帮助导入工具正确处理您的作品。
如果不确定文本使用的编码,可以尝试 ISO-8859-1(通常称为 Latin-1)或 Windows-1252(有时被误称为 ANSI),这两种编码在 Windows 程序的输出中非常常见。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '编码 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /users/edit 页面上“隐私偏好”弹窗的帮助文本。 */ function translatePrivacyPreferences() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Privacy preferences') { return; } if (container) { container.innerHTML = `此偏好设置允许您禁用一键分享按钮。该按钮可让她人将您的作品推荐到 Twitter、Tumblr 等外部网站。
请注意,一旦您在线发布了作品,读者仍可复制并粘贴链接到任何位置----如果您想限制对作品的访问,最佳方法是将作品锁定,仅限 Archive 注册用户查看。
启用此选项将允许其她 AO3 用户邀请您以共创者的身份列在作品、章节或系列中。在您接受邀请前,您不会在网站上任何地方以共创者身份出现。如果启用此选项,您可以在个人中心的“共创者请求”中查看收到的请求,并会收到一封通知邮件。
禁用此选项将阻止其她用户邀请您成为作品、章节或系列的共创者,您也不会收到任何通知。
更改此设置不会影响任何现有的共创作品。
要了解有关偏好设置及其含义的更多信息,请参阅我们的偏好常见问题。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '隐私偏好'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“显示偏好”弹窗的内容。 */ function translateDisplayPreferences() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Display preferences') { return; } if (container) { container.innerHTML = `站点界面可让您在登录账户时自定义浏览体验。不喜欢 Archive 的字体?您可以更改它们!不喜欢红色页眉?换成蓝色!请记住,在创建站点界面时,您只是在为自己更改 Archive ——其她用户将按照她们各自的界面看到 Archive 。换言之,站点界面可帮助您打造理想的个人浏览体验,而不会影响她人查看作品的方式。
作品界面可让您更改一个或多个作品在她人眼中的显示方式。作品界面仅影响作品正文——您无法更改 Archive 的导航或背景在其她用户那里显示的样式。但您可以创建自定义样式类,例如更改部分文字的颜色,或以特定方式缩进段落,等等。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面基础'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“作品标题格式”帮助弹窗。 */ function translateWorkTitleFormat() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work_title_format') { return; } if (container) { container.innerHTML = `指定在阅读作品时浏览器标签页标题的显示方式。示例:
启用此选项后,其她 AO3 用户可邀请您的作品加入其合集。在您接受邀请前,作品不会被添加。要了解有关接受合集邀请的更多信息,请参阅如何批准或拒绝包含我的作品的合集邀请。
禁用此选项将完全阻止她人邀请您的作品加入其合集,且您不会收到任何通知。
更改此设置不会影响已在合集中的现有作品。
筛选器列出了每个标签类别中最常用的十个标签。要使用其她标签进行筛选,请使用“要包括的其她标签”字段。
如果您感兴趣的标签不在前十个中,请在“要包括的其她标签”字段中开始输入所需标签——此处可使用所有标签类别,且可添加任意数量的标签。自动补全列表将帮助您找到标签的规范版本。可充分利用标签规范化结构排除所有关联作品(含子标签及同义标签)。
您也可以输入不在自动补全列表中的标签。如果您输入的标签已在 AO3 上使用但未标记为规范标签,则筛选器将查找使用您输入的确切标签的作品。如果您输入的标签在 AO3 上从未被使用,筛选器将进行简单的文本匹配,可能会带来意想不到的结果。“在结果中搜索”字段将更准确地进行文本匹配,尤其是在关系标签和其她包含“/”或其她非文字字符的标签的情况下。
从类别中选择任意标签,或在“要包括的其她标签”字段中输入标签,将与您选择的所有标签进行 AND 搜索。这意味着,如果您筛选女/女类别标签的作品,选择 青少年及以上 分级,在附加标签类别中选择规范的 Romance(爱情) 标签,并在“要包括的其她标签”字段中输入或选择规范的 Drama(剧情) 标签,则结果中只会包含同时带有所有这些标签的作品。
若要获取包含 标签A 或 标签B 的结果,请使用“在结果中搜索”字段。
要查看哪些标签为规范标签,请使用标签搜索。
要了解有关标签的更多信息,请参阅我们的标签常见问题。要查看标签整理者用于标记规范标签的指南或更好地理解 AO3 特有的标签术语,请阅读整理指南。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品筛选:包括标签'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译筛选侧边栏中的“排除标签”帮助文本。 */ function translateTagFiltersExcludeTags() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work filters exclude tags') { return; } if (container) { container.innerHTML = `筛选器列出了每个标签类别中最常用的十个标签。要使用其她标签进行筛选,请使用“要排除的其她标签”字段。
如果您想排除的标签不在前十个中,请在“要排除的其她标签”字段中开始输入所需标签——此处可使用所有标签类别,且可添加任意数量的标签。自动补全列表将帮助您找到标签的规范版本,可充分利用标签规范化结构排除所有关联作品(含子标签及同义标签)。
您也可以输入不在自动补全列表中的标签。如果您输入的标签已在 AO3 上使用但未标记为规范标签,则筛选器将查找使用您输入的确切标签的作品。如果您输入的标签在 AO3 上从未被使用,筛选器将进行简单的文本匹配,可能会带来意想不到的结果。“在结果中搜索”字段将更准确地进行文本匹配,尤其是在关系标签和其她包含“/”或其她非文字字符的标签的情况下。
从类别中选择任意标签,或在“要排除的其她标签”字段中输入标签,将与您选择的所有标签进行 OR 搜索。这意味着,如果您筛选女/女类别标签的作品,选择 主要角色死亡 预警,在附加标签类别中选择规范的 Alternate Universe(平行世界) 标签,并在“要排除的其她标签”字段中输入或选择规范的 Drama(剧情) 标签,则结果中只会包含不带有任何这些标签的作品。
要查看哪些标签为规范标签,请使用标签搜索。
要了解有关标签的更多信息,请参阅我们的标签常见问题。要查看标签整理者用于标记规范标签的指南或更好地理解 AO3 特有的标签术语,请阅读整理指南。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品筛选:排除标签'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“包含标签”筛选帮助的文本。 */ function translateBookmarkFiltersIncludeTags() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark filters include tags') { return; } if (container) { container.innerHTML = `筛选器列出了每个标签类别中最常用的十个标签。要使用其她标签进行筛选,请使用“要包括的其她作品标签”和“要包括的其她书签创建者标签”字段。
如果您感兴趣的标签不在前十个中,请在“要包括的其她作品标签”或“要包括的其她书签创建者标签”字段中开始输入所需标签——此处可使用所有标签类别,且可添加任意数量的标签。自动补全列表将帮助您找到标签的规范版本,可充分利用标签规范化结构排除所有关联作品(含子标签及同义标签)。
您也可以输入不在自动补全列表中的标签。如果您输入的标签已在 AO3 上使用但未标记为规范标签,则筛选器将查找使用您输入的确切标签的作品。如果您输入的标签在 AO3 上从未被使用,筛选器将进行简单的文本匹配,可能会带来意想不到的结果。“在结果中搜索”和“搜索书签创建者标签和注释”字段将更准确地进行文本匹配,尤其是在关系标签和其她包含“/”或其她非文字字符的标签的情况下。
从类别中选择任意标签,或在“要包括的其她作品标签”或“要包括的其她书签创建者标签”字段中输入标签,将与您选择的所有标签进行 AND 搜索。这意味着,如果您筛选女/女类别标签的书签,选择 青少年及以上 分级,在附加标签类别中选择规范的 Romance(爱情) 标签,并在“要包括的其她作品标签”字段中输入或选择规范的 Drama(剧情) 标签,结果中只会包含同时带有所有这些标签的作品或系列的书签。
若要获取包含标签 A 或 标签 B 的结果,请使用“在结果中搜索”或“搜索书签创建者标签和注释”字段。
要查看哪些标签为规范标签,请使用标签搜索。
要了解有关标签的更多信息,请参阅我们的标签常见问题。要查看标签整理者用于标记规范标签的指南或更好地理解 AO3 特有的标签术语,请阅读整理指南。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签筛选:包括标签'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /bookmarks/search 页面上“排除标签”筛选帮助的文本。 */ function translateBookmarkFiltersExcludeTags() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Bookmark filters exclude tags') { return; } if (container) { container.innerHTML = `筛选器列出了每个标签类别中最常用的十个标签。要使用其她标签进行筛选,请使用“要排除的其她作品标签”或“要排除的其她书签创建者标签”字段。
如果您想排除的标签不在前十个中,请在“要排除的其她作品标签”或“要排除的其她书签创建者标签”字段中开始输入所需标签——此处可使用所有标签类别,且可添加任意数量的标签。自动补全列表将帮助您找到标签的规范版本,可充分利用标签规范化结构排除所有关联作品(含子标签及同义标签)。
您也可以输入不在自动补全列表中的标签。如果您输入的标签已在 AO3 上使用但未标记为规范标签,则筛选器将查找使用您输入的确切标签的作品。如果您输入的标签在 AO3 上从未被使用,则筛选器将进行简单的文本匹配,可能会带来意想不到的结果。“在结果中搜索”和“搜索书签创建者标签和注释”字段将更准确地进行文本匹配,尤其是在关系标签和其她包含“/”或其她非文字字符的标签的情况下。
从类别中选择任意标签,或在“要排除的其她作品标签”或“要排除的其她书签创建者标签”字段中输入标签,将对您选择的所有标签执行 OR 搜索。这意味着,如果您筛选女/女类别标签的书签,选择 主要角色死亡 预警,在附加标签类别中选择规范的 Alternate Universe(平行世界) 标签,并在“要排除的其她作品标签”字段中输入或选择规范的 Drama(剧情) 标签,则结果中只会包含不带任何这些标签的书签。
要查看哪些标签为规范标签,请使用标签搜索。
要了解有关标签的更多信息,请参阅我们的标签常见问题。要查看标签整理者用于标记规范标签的指南或更好地理解 AO3 特有的标签术语,请阅读整理指南。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '书签筛选:排除标签'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /works/search 页面上“结果”相关的帮助文本。 */ function translateWorkSearchResultsHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Work search results help') { return; } if (container) { container.innerHTML = `符合条件的最新作品将显示在列表顶部。否则,列表将按相关性排序。如果作品数量众多,您可能需要更改搜索词而非翻页浏览结果。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '作品搜索:结果 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins approval”弹窗的提示信息。 */ function translateSkinsApprovalModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins approval') { return; } if (container) { container.innerHTML = `AO3 不再将新用户创建的界面添加到公共界面列表,因此您目前无法申请公开您的界面。此复选框仅供站点管理员将新的公共站点界面添加到列表中使用。但是,您仍然可以在公共站点界面和公共作品界面中使用用户创建的界面,也可以继续创建供个人使用的界面。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面审核'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins creating”弹窗中的 CSS 帮助文本。 */ function translateSkinsCreatingModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins creating') { return; } if (container) { container.innerHTML = `请注意,出于安全原因,您只能使用有限的 CSS 代码集:所有其她声明和注释都将被移除!
background, border, column, cue, flex, font, layer-background, layout-grid, list-style, margin, marker, outline, overflow, padding, page-break, pause, scrollbar, text, transform, transition
-replace, -use-link-source, accelerator, align-content, align-items, align-self, alignment-adjust, alignment-baseline, appearance, azimuth, baseline-shift, behavior, binding, bookmark-label, bookmark-level, bookmark-target, bottom, box-align, box-direction, box-flex, box-flex-group, box-lines, box-orient, box-pack, box-shadow, box-sizing, caption-side, clear, clip, color, color-profile, color-scheme, content, counter-increment, counter-reset, crop, cue, cue-after, cue-before, cursor, direction, display, dominant-baseline, drop-initial-after-adjust, drop-initial-after-align, drop-initial-before-adjust, drop-initial-before-align, drop-initial-size, drop-initial-value, elevation, empty-cells, filter, fit, fit-position, float, float-offset, font, font-effect, font-emphasize, font-emphasize-position, font-emphasize-style, font-family, font-size, font-size-adjust, font-smooth, font-stretch, font-style, font-variant, font-weight, grid-columns, grid-rows, hanging-punctuation, height, hyphenate-after, hyphenate-before, hyphenate-character, hyphenate-lines, hyphenate-resource, hyphens, icon, image-orientation, image-resolution, ime-mode, include-source, inline-box-align, justify-content, layout-flow, left, letter-spacing, line-break, line-height, line-stacking, line-stacking-ruby, line-stacking-shift, line-stacking-strategy, mark, mark-after, mark-before, marks, marquee-direction, marquee-play-count, marquee-speed, marquee-style, max-height, max-width, min-height, min-width, move-to, nav-down, nav-index, nav-left, nav-right, nav-up, opacity, order, orphans, page, page-policy, phonemes, pitch, pitch-range, play-during, position, presentation-level, punctuation-trim, quotes, rendering-intent, resize, rest, rest-after, rest-before, richness, right, rotation, rotation-point, ruby-align, ruby-overhang, ruby-position, ruby-span, size, speak, speak-header, speak-numeral, speak-punctuation, speech-rate, stress, string-set, tab-side, table-layout, target, target-name, target-new, target-position, top, unicode-bibi, unicode-bidi, user-select, vertical-align, visibility, voice-balance, voice-duration, oice-family, voice-pitch, voice-pitch-range, voice-rate, voice-stress, voice-volume, volume, white-space, white-space-collapse, widows, width, word-break, word-spacing, word-wrap, writing-mode, z-index
所有已批准的公共界面均可查看其代码,您可复制并编辑以供个人使用。
我们使用的 CSS 解析器仅保留每个属性的一个声明,这意味着像
.my-class {
background: -moz-linear-gradient(top, #1e5799 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%);
background: -o-linear-gradient(top, #1e5799 0%,#2989d8 50%,#207cca 51%,#7db9e8 100%);
background: -webkit-linear-gradient(top, #1e5799 0%,#2989d8 50%,#207cca 51%,#7db9e8 100%);
}
这样的规则集将只保留最后一个 background 声明(因此您的渐变效果仅在 WebKit 浏览器中显示)。为避免丢失重复属性的声明,请将每个声明拆分到独立的规则集中,如:
.my-class { background: -moz-linear-gradient(top, #1e5799 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%); }
.my-class { background: -o-linear-gradient(top, #1e5799 0%,#2989d8 50%,#207cca 51%,#7db9e8 100%); }
.my-class { background: -webkit-linear-gradient(top, #1e5799 0%,#2989d8 50%,#207cca 51%,#7db9e8 100%); }
遗憾的是,您不能在 CSS 中使用 font 简写。所有 font 属性必须分别指定,例如:font-size: 1.1em; font-weight: bold; font-family: Cambria, Constantia, Palatino, Georgia, serif;
在 font-family 属性中,我们允许您使用字母数字名称指定任何字体。您可以(但不必)使用单引号或双引号将名称括起,只需确保引号成对匹配。(例如,'Gill Sans' 和 "Gill Sans" 都可;'Gill Sans" 则不可。)请记住,字体必须安装在用户的操作系统中才能生效。建议在指定字体时添加备用字体,以防首选字体不可用。请参阅包含备用字体的网页安全字体集。
抱歉,我们不允许使用 @font-face 属性。如果您想在要分享的界面中使用不常见字体,建议在“描述”字段中添加注释,提供用户自行下载该字体的链接,并使用网页安全字体作为备用。
我们允许使用 JPG 、GIF 和 PNG 格式的外部图像 URL(格式如 url('https://example.com/my_awesome_image.jpg'))。但请注意,使用外部图像的界面将不会被批准为公共界面。
我们允许所有标准 CSS 关键词值(例如 absolute、bottom、center、underline 等)。
您可以指定最多两位小数的数值,作为百分比或各种单位:cm, em, ex, in, mm, pc, pt, px
PS:我们强烈建议学习并使用 em,它可以让您根据查看者当前的字体大小设置布局!这将使您的布局更加灵活,并响应不同的浏览器/字体设置。
您可以使用十六进制值(例如,#000000 表示十六进制黑色)或 RGB 、RGBA 值(例如 rgb(0,0,0) 和 rgba(0,0,0,0) 都表示黑色)指定颜色。这可能更安全,因为并非所有浏览器都一定支持所有颜色名称。但是,颜色名称更具可读性且易于记忆,因此我们也允许使用颜色名称。(建议您坚持使用常见支持的颜色名称集。)
您可以为 transform 属性指定 scale(数值) 形式的缩放,其中数值最多可指定两位小数。
CSS 中的注释会被移除。
一行 CSS 代码的格式类似:selector {property: value;}
selector 是要更改的 HTML 标签名称(如 body 或 h1),或已在标签上设置的 id 或 class。property 是您要更改的属性(例如字体大小),value 是您要设置的值。
示例:
body 标签内的字体大小略大于基线:body {font-size: 1.1em;}#header 的标签背景色为紫色:#header {background-color: purple}.meta 的标签文本闪烁(不建议使用):.meta {font-style: blink}一些有用的 CSS 教程:
如果您希望在特定情况下仅加载某段 CSS ,可以为界面创建一组特定的条件。只有满足条件时,我们才会加载该界面。可用的条件有:
此选项主要用于保持界面列表整洁。如果选择此项,您(及其她用户)将无法直接使用该界面:它仅作为母级被引用。即使公开,其也不会出现在主界面列表中,只会在引用它作为母级的界面说明中列出。这样可以方便地提供组件供她人使用,而不会因无法独立使用而在列表中产生混乱。:)
您可选择多个媒体类型。仅当所用设备支持该媒体类型时,才会加载对应样式表。例如,并非所有屏幕阅读器都会加载“speech”样式表。如果您的设备未加载界面,请改用“all”或“screen”,或提交支持请求以获取帮助。
留空则在所有浏览器上加载界面。选择后,仅在 Internet Explorer 浏览器中加载,可添加 IE 专用覆盖样式。
如果您同时使用母级界面,可为特定母级设置条件,然后创建针对不同浏览器表现不同的界面。例如,您可将大部分 CSS 放在一个母级界面,将 IE 专用样式放在另一个母级界面,将 handheld 媒体样式放在第三个母级界面,将 print 媒体样式放在第四个母级界面。最终界面将根据用户浏览器分别加载各母级!
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面条件'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins parents”弹窗的帮助文本。 */ function translateSkinsParentsModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins parents') { return; } if (container) { container.innerHTML = `您可以通过将一个站点界面设为另一个的母级来组合和分层多个站点界面。母级界面按顺序加载,以便按该顺序显示所有界面样式。要了解有关界面更多信息,请参阅界面与界面常见问题。
默认情况下,界面将在 Archive 默认样式之后加载。如果您不想如此,可以在“作用方式”菜单中指定将您的界面替换而不是添加到 Archive 默认样式。
如果您创建了替换界面,可能希望将组成当前默认 Archive 站点的所有界面作为母级一并加载。此选项仅在您从“作用方式”菜单中选择“完全替换 Archive 样式”时可用。之后,您可以编辑您的界面并删除不需要的部分。如果您要保留大部分内容,这将更容易操作,因为默认界面数量众多!
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面母级'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins wizard font”弹窗的帮助文本。 */ function translateSkinsWizardFontModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins wizard font') { return; } if (container) { container.innerHTML = `默认值为:'Lucida Grande'、'Lucida Sans Unicode'、Verdana、Helvetica、sans-serif、'GNU Unifont'
在此处输入任意字体名称,如果它已安装在您的计算机上,则可使用。如果您使用多种设备,请指定一些备用字体,名称之间用逗号分隔,以防某设备没有首选字体。
对于含有空格的字体名称,可使用单引号或双引号括起,例如 "Lucida Grande" 或 'Lucida Sans Unicode'。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面向导 字体'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins wizard font size”弹窗的帮助文本。 */ function translateSkinsWizardFontSizeModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins wizard font size') { return; } if (container) { container.innerHTML = `默认值为:100%
Archive 上的字体大小基于浏览器默认字体大小的百分比。使用小于 100 的数字可缩小 Archive 文本,使用大于 100 的数字可放大文本。输入 100 可保持 Archive 的默认字体大小。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面向导 字体大小'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins wizard vertical gap”弹窗的帮助文本。 */ function translateSkinsWizardVerticalGapModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins wizard vertical gap') { return; } if (container) { container.innerHTML = `默认值为:1.1286em
在此处输入任意数字,该数字将作为作品字体大小的倍数生效。数字越大,段落垂直间距越宽。
例如,大多数用户以 15 像素的字体大小查看作品。输入 2 将生成 30 像素的垂直间距,输入 0.5 则会产生约 8 像素的垂直间距。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面向导 垂直间距'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“Skins wizard accent color”弹窗的帮助文本。 */ function translateSkinsWizardAccentColorModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Skins wizard accent color') { return; } if (container) { container.innerHTML = `默认值为:#ddd
替换 Archive 中多个位置使用的灰色,包括表单背景、主导航中的下拉菜单,以及个人中心页面的“同人圈”和“最近作品”部分。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '界面向导 强调色'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用于翻译“合集名称”帮助弹窗 */ function translateCollectionNameHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Collection name') { return; } if (container) { container.innerHTML = `合集名称可以在以后更改,但这样会破坏指向该合集的链接。
名称只能由 ASCII 字母(a-z、A-Z)、数字和下划线组成,且不能包含空格。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '合集名称'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 "Icon Alt Text" 弹窗。 */ function translateIconAltTextHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const header = container?.querySelector('h4'); if (!header || header.textContent !== 'Icon Alt Text') { return; } container.innerHTML = `替代文本的作用是在图像无法显示时解释其含义。该功能供关闭图像显示或使用屏幕阅读器的视障用户使用。请勿将替代文本用于标注图片来源!
例如,AO3 标志的替代文本为:“Archive of Our Own”。
`; container.setAttribute('data-translated-by-custom-function', 'true'); const footer = container.nextElementSibling; if (footer) { const footerTitle = footer.querySelector('span.title'); if (footerTitle) { footerTitle.textContent = '图标替代文本'; } const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } } /** * 专用于翻译“笔名图标注释”帮助弹窗 */ function translatePseudIconCommentHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Pseud icon comment') { return; } if (container) { container.innerHTML = `您可以在此处填写关于您的图标的额外信息,例如图标制作者的署名。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '笔名图标注释文本'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用于翻译“审核制合集”帮助弹窗 */ function translateCollectionModeratedHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Collection moderated') { return; } if (container) { container.innerHTML = `默认情况下,合集非审核制,这意味着任何注册用户都可以将其作品添加到合集。合集的所有者/管理员仍可在作品发布后拒绝不适当的作品。
如果您将合集设置为审核制,所有注册用户仍可发布作品,但在获得管理员或所有者批准之前,作品不会出现在合集内。认证成员投稿将自动通过审核(无需人工操作)。
合集的所有者可编辑合集偏好和数据,也可完全删除合集。合集的管理员可批准/邀请成员并添加或拒绝作品。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '合集审核制'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用于翻译“关闭的合集”帮助弹窗 */ function translateCollectionClosedHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Collection closed') { return; } if (container) { container.innerHTML = `一旦合集关闭,除维护者(所有者和管理员)外,无法再添加作品或书签。如果这是赠文交换或其她活动,请注意,这不会自动根据您在活动设置中设定的任何截止日期触发,必须在此手动设置。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '合集已关闭'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译 /tags/search 页面上“标签搜索结果”帮助文本。 */ function translateTagSearchResultsHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Tag search results help') { return; } if (container) { container.innerHTML = `高亮标签为规范标签。
最新标签将显示在列表顶部。其余标签按类型和名称字母顺序排序。
如果标签过多,请尝试优化搜索,而不是翻页浏览结果。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '标签搜索:结果 帮助'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译挑战注册页面上“选择任意”的帮助弹窗。 */ function translateChallengeAnyTips() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Challenge any') { return; } if (container) { container.innerHTML = `如果您在报名时为某个字段选择“任意”,即表示您同意在该字段上进行无条件匹配——此操作存在潜在风险!请务必确保您真正接受任意内容!即使您在该字段填写了具体选项,“任意”也将覆盖所有已填写内容。
提供“任意”:
请求“任意”(此情况常易混淆!):
挑战活动管理员可能会选择仅允许在“提供”中使用“任意”选项,或仅开放特定字段使用。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '挑战活动 任意'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译挑战注册页面上“可选标签”的帮助弹窗。 */ function translateOptionalTagsHelp() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Challenge optional tags user') { return; } if (container) { container.innerHTML = `管理员将使用可选标签尝试优化匹配,但必要时可能完全忽略这些标签以完成匹配。此处适合添加冷门或特定标签。 请注意:您添加的标签越多,可选标签被忽略的可能性越大,且匹配运行速度越慢,因此请谨慎添加!
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '挑战活动可选标签 用户'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专门用于翻译“章节标题”帮助弹窗。 */ function translateChapterTitleHelpModal() { const container = document.querySelector('#modal div.content.userstuff'); const footer = container?.nextElementSibling; const footerTitle = footer?.querySelector('span.title'); if (!footerTitle || footerTitle.textContent !== 'Chapter title') { return; } if (container) { container.innerHTML = `您可以为章节添加标题,但这不是必填项。
`; container.setAttribute('data-translated-by-custom-function', 'true'); } footerTitle.textContent = '章节标题'; const closeButton = footer.querySelector('a.modal-closer'); if (closeButton) { closeButton.textContent = '关闭'; } } /** * 专用翻译函数:翻译“关于 OTW”页面 */ function translateAboutPage() { const mainDiv = document.querySelector('div#main.about'); if (!mainDiv) return; const titleElement = mainDiv.querySelector('h2.heading'); if (!titleElement || !titleElement.textContent.includes('About the OTW')) { return; } mainDiv.innerHTML = `再创作组织(OTW)是一个由同人爱好者于 2007 年创立的非营利组织,旨在通过提供多种形式的同人作品和同人文化的访问权限并保存其历史,来服务同人爱好者的利益。我们相信,同人作品具有再创作性,而再创作作品具有合法地位。
我们积极且富有创新精神,致力于保护和捍卫我们的作品免受商业剥削和法律挑战。我们通过保护和培育同人爱好者社群、作品、评论、历史及身份认同,同时为所有同人爱好者提供尽可能广泛的同人活动参与途径,从而维护我们的同人经济、价值观和创作表达。
Archive of Our Own 采用开源归档技术,为同人作品提供一个非商业、非营利的集中托管平台。欢迎您为我们的 GitHub 代码库做出贡献,相关开放任务清单可在我们的 Jira 项目页面查阅。
我们的其她主要项目包括:
您可通过官网 transformativeworks.org 了解更多关于 OTW 及其项目的信息,也可以在常见问题页面上了解您的资助对 OTW 持续发展和扩展的重要性。如果您有媒体或研究方面的问题,请联系通讯团队。
再创作组织(OTW)是 Archive of Our Own(AO3)的上级组织。我们持续招募志愿者参与项目开发。若您有意为 AO3 提供志愿服务,可关注以下委员会:无障碍、设计与技术委员会(AD&T);AO3 文档委员会;政策与滥用委员会;支持团队;标签管理委员会;以及翻译委员会。
同时诚邀您为我们的 GitHub 代码库贡献代码,开放任务详见 Jira 项目。欢迎浏览我们的志愿者职位列表,订阅邮件以获取含志愿者招募的全面资讯,并申请符合您资历和兴趣的任何志愿者职位。
AO3 的日常运营需要持续支出——服务器的电力和带宽——以及随着用户和作品数量的增加,不时购买新服务器等一次性支出。任何向 OTW 的捐赠都至关重要。(请放心,我们绝不会将您的 AO3 用户名与财务信息关联。)
无论您的外表、境遇、立场或世界观如何:只要您喜欢欣赏、创作或评论同人作品,AO3 即为您而建。
本站是一个由同人爱好者为同人爱好者打造的永久性全同人圈作品托管平台。无论您以何种方式使用本站,您都是其中的一份子,通过您的使用和反馈为其注入活力并塑造未来。
我们——AO3 团队——深知无法在初次尝试时就尽善尽美,也无法让所有人都满意。但我们会努力寻求平衡,郑重考虑并认真对待您的每一条反馈。
您可以自由发挥创意,但必须遵守一些必要的限制,以便为其她用户提供可行性服务。本站致力于保护您的自由表达权及隐私权;详情请阅读我们的服务条款。
我们明白,要让 AO3 真正实现全同人圈愿景,仍需完善关键功能:如托管文本形式以外的同人作品、提供多语言界面、增加用户互动方式等。但有了您的支持,我们终将实现目标。
我们之所以构建这座档案馆,是因为我们相信持不同观点与主张的人可以齐聚一堂,彼此分享。
我们为您而建,期待您成为其中的一员。
本文是对 Dreamwidth 多元化声明的再创作。
本作品采用知识共享署名-相同方式共享 3.0 未本地化版本许可协议进行许可。
${line}
`).join('')}]*)>/gi, '')
.replace(/<\/code>/gi, ' ')
.replace(/]*)>/gi, '')
.replace(/<\/pre>/gi, ' ')
.replace(/]*)>/gi, '')
.replace(/<\/kbd>/gi, ' ');
}
/**
* 还原被伪装的 HTML 标签
*/
function unmaskProtectedTags(html) {
if (!html) return html;
return html
.replace(/]*)>/gi, '')
.replace(/<\/v-tr-code>/gi, '')
.replace(/]*)>/gi, '')
.replace(/<\/pre>/gi, '
')
.replace(/]*)>/gi, '')
.replace(/<\/v-tr-kbd>/gi, '');
}
/**
* 处理对谷歌翻译接口的特定请求流程
*/
async function _handleGoogleRequest(engineConfig, paragraphs, fromLang, toLang) {
await GoogleTranslateHelper.findAuth();
if (!GoogleTranslateHelper.translateAuth) {
throw new Error('无法获取谷歌翻译的授权凭证');
}
const headers = {
...engineConfig.headers,
'X-goog-api-key': GoogleTranslateHelper.translateAuth
};
const sourceTexts = paragraphs.map(p => maskProtectedTags(p.outerHTML));
const requestData = JSON.stringify([
[sourceTexts, fromLang, toLang], "te"
]);
Logger.info('网络', '发起请求: 谷歌翻译', {
url: engineConfig.url_api,
from: fromLang,
to: toLang,
paragraphs: paragraphs.length
});
const res = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: engineConfig.method,
url: engineConfig.url_api,
headers: headers,
data: requestData,
responseType: 'json',
timeout: 45000,
onload: resolve,
onerror: () => reject(new Error('网络请求错误')),
ontimeout: () => reject(new Error('请求超时'))
});
});
if (res.status !== 200) {
throw new Error(`谷歌翻译 API 错误 (代码: ${res.status}): ${res.statusText}`);
}
const translatedHtmlSnippets = getNestedProperty(res.response, '0');
if (!translatedHtmlSnippets || !Array.isArray(translatedHtmlSnippets)) {
throw new Error('从谷歌翻译接口返回的响应结构无效');
}
return translatedHtmlSnippets.map(html => unmaskProtectedTags(html));
}
/**
* 处理对微软翻译接口的特定请求流程
*/
async function _handleBingRequest(engineConfig, paragraphs, fromLang, toLang, isRetry = false) {
const token = await BingTranslateHelper.getToken();
const bingFrom = BING_LANG_CODE_MAP[fromLang] || fromLang;
const bingTo = BING_LANG_CODE_MAP[toLang] || toLang;
let url = `${engineConfig.url_api}&to=${bingTo}`;
if (bingFrom !== 'auto-detect') {
url += `&from=${bingFrom}`;
}
const requestBody = JSON.stringify(paragraphs.map(p => ({
text: p.innerHTML
})));
if (!isRetry) {
Logger.info('网络', '发起请求: 微软翻译', {
url: url,
from: bingFrom,
to: bingTo,
paragraphs: paragraphs.length
});
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
data: requestBody,
responseType: 'json',
timeout: 45000,
onload: async (res) => {
if (res.status === 401 && !isRetry) {
Logger.warn('网络', '微软翻译 Token 过期,正在重试');
BingTranslateHelper.clearToken();
try {
const retryResult = await _handleBingRequest(engineConfig, paragraphs, fromLang, toLang, true);
resolve(retryResult);
} catch (retryError) {
reject(retryError);
}
return;
}
if (res.status !== 200) {
const e = new Error(`Microsoft API Error: ${res.status} ${res.statusText}`);
e.type = res.status === 429 ? 'rate_limit' : 'api_error';
reject(e);
return;
}
const responseData = res.response;
if (!Array.isArray(responseData)) {
const e = new Error('Invalid response format');
e.type = 'invalid_json';
reject(e);
return;
}
resolve(responseData.map(item => item.translations[0].text));
},
onerror: (err) => {
const e = new Error('Network Error');
e.type = 'network';
reject(e);
},
ontimeout: () => {
const e = new Error('Timeout');
e.type = 'timeout';
reject(e);
}
});
});
}
/**
* OpenAI 的专属错误处理策略
*/
function _handleOpenaiError(res, name, responseData) {
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || res.statusText;
const apiErrorCode = getNestedProperty(responseData, 'error.code');
let userFriendlyError;
const error = new Error();
error.noRetry = false;
switch (res.status) {
case 400:
if (apiErrorCode === 'model_not_found') {
userFriendlyError = `模型不存在 (400):您选择的模型当前不可用或您无权访问。请在设置中更换模型。`;
} else {
userFriendlyError = `错误的请求 (400):请求的格式或参数有误。`;
}
error.noRetry = true;
break;
case 401:
userFriendlyError = `API Key 无效或认证失败 (401):请在设置面板中检查您的 ${name} API Key。`;
error.noRetry = true;
break;
case 403:
userFriendlyError = `权限被拒绝 (403):您的 API Key 无权访问所请求的资源,或您所在的地区不受支持。`;
error.noRetry = true;
break;
case 404:
userFriendlyError = `资源未找到 (404):请求的 API 端点不存在。`;
error.noRetry = true;
break;
case 429:
if (apiErrorCode === 'insufficient_quota') {
userFriendlyError = `账户余额不足 (429):您的 ${name} 账户已用尽信用点数或达到支出上限。请前往服务官网检查您的账单详情。`;
error.noRetry = true;
error.type = 'billing_error';
} else {
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
}
break;
case 500:
userFriendlyError = `服务器内部错误 (500):${name} 的服务器遇到问题。`;
error.type = 'server_overloaded';
break;
case 503:
if (apiErrorMessage && apiErrorMessage.includes('Slow Down')) {
userFriendlyError = `服务暂时过载 (503 - Slow Down):由于您的请求速率突然增加,服务暂时受到影响。`;
} else {
userFriendlyError = `服务器当前过载 (503):${name} 的服务器正经历高流量。`;
}
error.type = 'server_overloaded';
break;
default:
userFriendlyError = `发生未知 API 错误 (代码: ${res.status})。`;
error.noRetry = true;
break;
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* Anthropic 的专属错误处理策略
*/
function _handleAnthropicError(res, name, responseData) {
const apiErrorType = getNestedProperty(responseData, 'error.type');
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || res.statusText;
let userFriendlyError;
const error = new Error();
error.noRetry = false;
switch (apiErrorType) {
case 'invalid_request_error':
userFriendlyError = `无效请求 (${res.status}):请求的格式或参数有误。如果问题持续,可能是模型名称不受支持或已更新。`;
error.noRetry = true;
break;
case 'authentication_error':
userFriendlyError = `API Key 无效或认证失败 (401):请在设置面板中检查您的 ${name} API Key。`;
error.noRetry = true;
break;
case 'permission_error':
userFriendlyError = `权限被拒绝 (403):您的 API Key 无权访问所请求的资源。`;
error.noRetry = true;
break;
case 'not_found_error':
userFriendlyError = `资源未找到 (404):请求的 API 端点或模型不存在。`;
error.noRetry = true;
break;
case 'request_too_large':
userFriendlyError = `请求内容过长 (413):发送的文本量超过了 API 的单次请求上限。`;
error.noRetry = true;
break;
case 'rate_limit_error':
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
break;
case 'api_error':
userFriendlyError = `服务器内部错误 (500):${name} 的服务器遇到问题。`;
error.type = 'server_overloaded';
break;
case 'overloaded_error':
userFriendlyError = `服务器过载 (529):${name} 的服务器当前负载过高。`;
error.type = 'server_overloaded';
break;
default:
if (res.status === 413) {
userFriendlyError = `请求内容过长 (413):发送的文本量超过了 API 的单次请求上限。`;
} else {
userFriendlyError = `发生未知 API 错误 (代码: ${res.status})。`;
}
error.noRetry = true;
break;
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* Zhipu AI 的专属错误处理策略
*/
function _handleZhipuAiError(res, name, responseData) {
const businessErrorCode = getNestedProperty(responseData, 'error.code');
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || res.statusText;
let userFriendlyError;
const error = new Error();
if (businessErrorCode) {
switch (businessErrorCode) {
case '1001':
case '1002':
case '1003':
case '1004':
userFriendlyError = `API Key 无效或认证失败 (${businessErrorCode}):请在设置面板中检查您的 ${name} API Key 是否正确填写。`;
error.noRetry = true;
break;
case '1112':
userFriendlyError = `账户异常 (${businessErrorCode}):您的 ${name} 账户已被锁定,请联系平台客服。`;
error.noRetry = true;
break;
case '1113':
userFriendlyError = `账户余额不足 (${businessErrorCode}):您的 ${name} 账户已欠费,请前往 Zhipu AI 官网充值。`;
error.noRetry = true;
break;
case '1301':
userFriendlyError = `内容安全策略阻止 (${businessErrorCode}):因含有敏感内容,请求被 Zhipu AI 安全策略阻止。`;
error.noRetry = true;
error.type = 'content_error';
break;
case '1302':
case '1303':
error.message = `请求频率过高 (${businessErrorCode}):已超出 API 的速率限制。\n\n原始错误信息:\n${apiErrorMessage}`;
error.type = 'rate_limit';
return error;
case '1304':
userFriendlyError = `调用次数超限 (${businessErrorCode}):已达到当日调用次数限额,请联系 Zhipu AI 客服。`;
error.noRetry = true;
break;
default:
userFriendlyError = `发生未知的业务错误 (代码: ${businessErrorCode})。`;
error.noRetry = true;
break;
}
} else {
return new BaseApiClient({ name })._handleError(res, responseData);
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* DeepSeek AI 的专属错误处理策略
*/
function _handleDeepseekAiError(res, name, responseData) {
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || getNestedProperty(responseData, 'message') || res.statusText;
let userFriendlyError;
const error = new Error();
switch (res.status) {
case 400:
case 422:
userFriendlyError = `请求格式或参数错误 (${res.status}):请检插件是否为最新版本。如果问题持续,可能是 API 服务端出现问题。`;
error.noRetry = true;
break;
case 401:
userFriendlyError = `API Key 无效或认证失败 (401):请在设置面板中检查您的 ${name} API Key 是否正确填写。`;
error.noRetry = true;
break;
case 402:
userFriendlyError = `账户余额不足 (402):您的 ${name} 账户余额不足。请前往 DeepSeek 官网充值。`;
error.noRetry = true;
break;
case 429:
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
break;
case 500:
userFriendlyError = `服务器内部故障 (500):${name} 的服务器遇到未知问题。`;
error.type = 'server_overloaded';
break;
case 503:
userFriendlyError = `服务器繁忙 (503):${name} 的服务器当前负载过高。`;
error.type = 'server_overloaded';
break;
default:
return new BaseApiClient({ name })._handleError(res, responseData);
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* Google AI 的专属错误处理策略
*/
function _handleGoogleAiError(errorData) {
const { type, message, res, name } = errorData;
const error = new Error();
let userFriendlyError;
if (type === 'content_error') {
userFriendlyError = `内容安全策略阻止:${message}。请尝试修改原文内容。`;
error.noRetry = true;
} else if (type === 'key_invalid') {
userFriendlyError = `API Key 无效或认证失败:${message}。请在设置面板中检查您的 API Key。`;
error.noRetry = true;
} else if (res) {
switch (res.status) {
case 400:
userFriendlyError = `请求格式错误 (400):您的国家/地区可能不支持 Gemini API 的免费套餐,请在 Google AI Studio 中启用结算。`;
error.noRetry = true;
break;
case 429:
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
break;
default:
return new BaseApiClient({ name })._handleError(res, res.response);
}
} else {
userFriendlyError = `发生未知错误:${message}`;
error.noRetry = (type !== 'network' && type !== 'timeout');
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${message}`;
return error;
}
/**
* SiliconFlow 的专属错误处理策略
*/
function _handleSiliconFlowError(res, name, responseData) {
const apiErrorMessage = getNestedProperty(responseData, 'message') || res.statusText;
const apiErrorCode = getNestedProperty(responseData, 'code');
let userFriendlyError;
const error = new Error();
switch (res.status) {
case 400:
if (apiErrorCode === 20012) {
userFriendlyError = `模型不存在 (400):您选择的模型名称无效或已下线。请在设置中更换模型。`;
} else {
userFriendlyError = `请求参数错误 (400):请检查插件版本或配置。`;
}
error.noRetry = true;
break;
case 401:
userFriendlyError = `API Key 无效 (401):请在设置面板中检查您的 ${name} API Key。`;
error.noRetry = true;
break;
case 403:
userFriendlyError = `权限不足或余额不足 (403):可能是账户余额不足,或该模型需要实名认证。请前往 SiliconFlow 官网检查账户状态。`;
error.noRetry = true;
break;
case 429:
userFriendlyError = `请求频率过高 (429):触发了速率限制 (RPM/TPM)。`;
error.type = 'rate_limit';
break;
case 503:
case 504:
userFriendlyError = `服务系统负载高 (${res.status}):SiliconFlow 服务器暂时繁忙。`;
error.type = 'server_overloaded';
break;
case 500:
userFriendlyError = `服务器内部错误 (500):服务发生了未知错误。`;
error.type = 'server_overloaded';
break;
default:
userFriendlyError = `发生未知 API 错误 (代码: ${res.status})。`;
error.noRetry = true;
break;
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* Groq AI 的专属错误处理策略
*/
function _handleGroqAiError(res, name, responseData) {
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || getNestedProperty(responseData, 'message') || res.statusText;
let userFriendlyError;
const error = new Error();
switch (res.status) {
case 400:
userFriendlyError = `请求无效 (400):请求语法错误。请检查请求格式。`;
error.noRetry = true;
break;
case 401:
userFriendlyError = `API Key 无效或认证失败 (401):请在设置面板中检查您的 ${name} API Key。`;
error.noRetry = true;
break;
case 403:
userFriendlyError = `权限被拒绝 (403):您的网络或 API Key 无权访问所请求的资源。`;
error.noRetry = true;
break;
case 404:
userFriendlyError = `资源未找到 (404):请求的模型或端点不存在。请检查模型名称或接口地址。`;
error.noRetry = true;
break;
case 413:
userFriendlyError = `请求内容过长 (413):发送的文本量超过了限制。请尝试减少单次翻译的文本量。`;
error.noRetry = true;
break;
case 422:
userFriendlyError = `无法处理的实体 (422):请求格式正确但包含语义错误。`;
error.noRetry = true;
break;
case 424:
userFriendlyError = `依赖失败 (424):依赖请求失败(可能是 Remote MCP 认证问题)。`;
error.noRetry = true;
break;
case 429:
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
break;
case 498:
userFriendlyError = `Flex Tier 容量超限 (498):当前 Flex Tier 已满。`;
error.type = 'server_overloaded';
break;
case 500:
userFriendlyError = `服务器内部错误 (500):${name} 服务器发生通用错误。`;
error.type = 'server_overloaded';
break;
case 502:
userFriendlyError = `网关错误 (502):上游服务器响应无效。`;
error.type = 'server_overloaded';
break;
case 503:
userFriendlyError = `服务不可用 (503):服务器正在维护或过载。`;
error.type = 'server_overloaded';
break;
default:
userFriendlyError = `发生未知 API 错误 (代码: ${res.status})。`;
error.noRetry = true;
break;
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* Cerebras AI 的专属错误处理策略
*/
function _handleCerebrasAiError(res, name, responseData) {
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || getNestedProperty(responseData, 'message') || res.statusText;
let userFriendlyError;
const error = new Error();
switch (res.status) {
case 400:
userFriendlyError = `请求无效 (400):请求参数有误。请检查模型名称或输入格式。`;
error.noRetry = true;
break;
case 401:
userFriendlyError = `认证失败 (401):API Key 无效或缺失。请在设置面板中检查您的 ${name} API Key。`;
error.noRetry = true;
break;
case 402:
userFriendlyError = `需要付款 (402):账户余额不足或需要充值。`;
error.noRetry = true;
break;
case 403:
userFriendlyError = `权限被拒绝 (403):无权访问该资源。`;
error.noRetry = true;
break;
case 404:
userFriendlyError = `资源未找到 (404):请求的模型或端点不存在。`;
error.noRetry = true;
break;
case 408:
userFriendlyError = `请求超时 (408):服务器处理请求超时。`;
error.type = 'timeout';
break;
case 409:
userFriendlyError = `请求冲突 (409):资源状态冲突。`;
error.type = 'server_overloaded';
break;
case 422:
userFriendlyError = `无法处理的实体 (422):请求格式正确但包含语义错误(如无效的模型参数)。`;
error.noRetry = true;
break;
case 429:
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
break;
default:
if (res.status >= 500) {
userFriendlyError = `服务器内部错误 (${res.status}):${name} 服务器发生错误。`;
error.type = 'server_overloaded';
} else {
userFriendlyError = `发生未知 API 错误 (代码: ${res.status})。`;
error.noRetry = true;
}
break;
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* Together AI 的专用错误处理策略
*/
function _handleTogetherAiError(res, name, responseData) {
const apiErrorMessage = getNestedProperty(responseData, 'error.message') || getNestedProperty(responseData, 'message') || res.statusText;
let userFriendlyError;
const error = new Error();
switch (res.status) {
case 400:
case 422:
userFriendlyError = `请求格式或参数错误 (${res.status}):请检查插件是否为最新版本。如果问题持续,可能是 API 服务端出现问题。`;
error.noRetry = true;
break;
case 401:
userFriendlyError = `API Key 无效或认证失败 (401):请在设置面板中检查您的 ${name} API Key 是否正确填写。`;
error.noRetry = true;
break;
case 402:
userFriendlyError = `需要付费 (402):您的 ${name} 账户已达到消费上限或需要充值。请检查您的账户账单设置。`;
error.noRetry = true;
break;
case 403:
case 413:
userFriendlyError = `请求内容过长 (${res.status}):发送的文本量超过了模型的上下文长度限制。请尝试翻译更短的文本段落。`;
error.noRetry = true;
break;
case 404:
userFriendlyError = `模型或接口地址不存在 (404):您选择的模型名称可能已失效,或接口地址不正确。请尝试在设置面板中切换至其她模型或检查接口地址。`;
error.noRetry = true;
break;
case 429:
userFriendlyError = `请求频率过高 (429):已超出 API 的速率限制。`;
error.type = 'rate_limit';
break;
case 500:
userFriendlyError = `服务器内部错误 (500):${name} 的服务器遇到问题。`;
error.type = 'server_overloaded';
break;
case 502:
userFriendlyError = `网关错误 (502):上游服务器响应无效。这通常是临时问题。`;
error.type = 'server_overloaded';
break;
case 503:
userFriendlyError = `服务过载 (503):${name} 的服务器当前流量过高。`;
error.type = 'server_overloaded';
break;
default:
return new BaseApiClient({ name })._handleError(res, responseData);
}
error.message = userFriendlyError + `\n\n原始错误信息:\n${apiErrorMessage}`;
return error;
}
/**
* API 错误处理策略注册表
*/
const API_ERROR_HANDLERS = {
'openai': _handleOpenaiError,
'siliconflow': _handleSiliconFlowError,
'anthropic': _handleAnthropicError,
'zhipu_ai': _handleZhipuAiError,
'deepseek_ai': _handleDeepseekAiError,
'google_ai': _handleGoogleAiError,
'groq_ai': _handleGroqAiError,
'together_ai': _handleTogetherAiError,
'cerebras_ai': _handleCerebrasAiError,
'modelscope_ai': _handleTogetherAiError
};
/**************************************************************************
* 术语表系统、工具函数与核心逻辑
**************************************************************************/
/**
* 为词形变体创建正则表达式
*/
function createSmartRegexPattern(forms) {
if (!forms || forms.size === 0) {
return '';
}
const sortedForms = Array.from(forms).sort((a, b) => b.length - a.length);
const escapedForms = sortedForms.map(form =>
form.replace(/([.*+?^${}()|[\]\\])/g, '\\$&')
);
const pattern = escapedForms.join('|');
const longestForm = sortedForms[0];
const startsWithWordChar = /^[a-zA-Z0-9_]/.test(longestForm);
const endsWithWordChar = /[a-zA-Z0-9_]$/.test(longestForm);
const prefix = startsWithWordChar ? '\\b' : '';
const suffix = endsWithWordChar ? '\\b' : '';
return `${prefix}(?:${pattern})${suffix}`;
}
/**
* 生成一个随机的6位数字字符串
*/
function generateRandomPlaceholder() {
const chars = '0123456789';
let result = '';
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 在DOM节点内查找一个由多部分文本组成的、有序的邻近序列
*/
function findOrderedDOMSequence(rootNode, rule) {
const { parts: partsWithForms, isGeneral } = rule;
const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
if (node.parentElement.closest('[data-glossary-applied="true"]')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
if (textNodes.length === 0) return null;
for (let i = 0; i < textNodes.length; i++) {
for (let j = 0; j < textNodes[i].nodeValue.length; j++) {
const matchResult = findSequenceFromPosition(i, j);
if (matchResult) {
return matchResult;
}
}
}
return null;
function findSequenceFromPosition(startNodeIndex, startOffset) {
let currentNodeIndex = startNodeIndex;
let currentOffset = startOffset;
const matchedWords = [];
const endPoints = [];
for (let partIndex = 0; partIndex < partsWithForms.length; partIndex++) {
const currentPartForms = partsWithForms[partIndex].sort((a, b) => b.length - a.length);
let bestMatch = null;
let searchStr = textNodes[currentNodeIndex].nodeValue.substring(currentOffset);
let lookaheadIndex = currentNodeIndex + 1;
while (lookaheadIndex < textNodes.length && searchStr.length < 200) {
searchStr += textNodes[lookaheadIndex].nodeValue;
lookaheadIndex++;
}
const textToSearch = isGeneral ? searchStr.toLowerCase() : searchStr;
for (const form of currentPartForms) {
const formToMatch = isGeneral ? form.toLowerCase() : form;
if (textToSearch.startsWith(formToMatch)) {
const prevChar = (startNodeIndex === 0 && startOffset === 0) ? ' ' : textNodes[startNodeIndex].nodeValue[startOffset - 1] || ' ';
if (partIndex === 0 && /[a-zA-Z0-9]/.test(prevChar)) {
continue;
}
bestMatch = form;
break;
}
}
if (bestMatch) {
matchedWords.push(bestMatch);
let consumedLength = bestMatch.length;
currentOffset += consumedLength;
while (currentOffset >= textNodes[currentNodeIndex].nodeValue.length && currentNodeIndex < textNodes.length - 1) {
currentOffset -= textNodes[currentNodeIndex].nodeValue.length;
currentNodeIndex++;
}
endPoints.push({ nodeIndex: currentNodeIndex, offset: currentOffset });
if (partIndex < partsWithForms.length - 1) {
let separatorFound = false;
while (currentNodeIndex < textNodes.length) {
const remainingInNode = textNodes[currentNodeIndex].nodeValue.substring(currentOffset);
const separatorMatch = remainingInNode.match(/^[\s--﹣—–]+/);
if (separatorMatch) {
currentOffset += separatorMatch[0].length;
separatorFound = true;
break;
}
if (remainingInNode.trim() !== '') {
return null;
}
currentNodeIndex++;
currentOffset = 0;
if (currentNodeIndex < textNodes.length) {
separatorFound = true;
} else {
return null;
}
}
if (!separatorFound) return null;
}
} else {
return null;
}
}
const finalEndPoint = endPoints[endPoints.length - 1];
const nextChar = textNodes[finalEndPoint.nodeIndex].nodeValue[finalEndPoint.offset] || ' ';
if (/[a-zA-Z0-9]/.test(nextChar)) {
return null;
}
return {
startNode: textNodes[startNodeIndex],
startOffset: startOffset,
endNode: textNodes[finalEndPoint.nodeIndex],
endOffset: finalEndPoint.offset,
matchedWords: matchedWords
};
}
}
/**
* 在DOM节点内查找一个由多部分文本组成的、无序但邻近的序列
*/
function findUnorderedDOMSequence(rootNode, rule) {
const { parts: partsWithForms, isGeneral } = rule;
const HTML_TAG_PLACEHOLDER = '\u0001';
const ALLOWED_SEPARATORS_REGEX = /^[\s\u0001--﹣—–]*$/;
const WORD_CHAR_REGEX = /[a-zA-Z0-9]/;
const MAX_DISTANCE_FACTOR = 2.5;
const MAX_DISTANCE_BASE = 30;
const textMap = [];
let normalizedText = '';
const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
if (node.parentElement.closest('[data-glossary-applied="true"]')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
let node;
while ((node = walker.nextNode())) {
if (node.nodeType === Node.TEXT_NODE) {
const nodeValue = node.nodeValue;
for (let i = 0; i < nodeValue.length; i++) {
textMap.push({ node: node, offset: i });
}
normalizedText += nodeValue;
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (['EM', 'STRONG', 'B', 'I', 'U', 'SPAN', 'CODE'].includes(node.tagName)) {
textMap.push({ node: node, offset: -1 });
normalizedText += HTML_TAG_PLACEHOLDER;
}
}
}
if (!normalizedText.trim()) return null;
const searchText = isGeneral ? normalizedText.toLowerCase() : normalizedText;
const originalTermLength = partsWithForms.map(p => p[0]).join(' ').length;
const maxDistance = Math.max(originalTermLength * MAX_DISTANCE_FACTOR, MAX_DISTANCE_BASE);
const partPositions = partsWithForms.map(partSet => {
const positions = [];
for (const form of partSet) {
const term = isGeneral ? form.toLowerCase() : form;
let lastIndex = -1;
while ((lastIndex = searchText.indexOf(term, lastIndex + 1)) !== -1) {
positions.push({ start: lastIndex, end: lastIndex + term.length });
}
}
return positions;
});
if (partPositions.some(p => p.length === 0)) {
return null;
}
function getCombinations(arr) {
if (arr.length === 1) {
return arr[0].map(item => [item]);
}
const result = [];
const allCasesOfRest = getCombinations(arr.slice(1));
for (let i = 0; i < allCasesOfRest.length; i++) {
for (let j = 0; j < arr[0].length; j++) {
result.push([arr[0][j]].concat(allCasesOfRest[i]));
}
}
return result;
}
const allCombinations = getCombinations(partPositions);
for (const combination of allCombinations) {
combination.sort((a, b) => a.start - b.start);
const overallStart = combination[0].start;
const overallEnd = combination[combination.length - 1].end;
if (overallEnd - overallStart > maxDistance) {
continue;
}
let isValid = true;
for (let i = 0; i < combination.length - 1; i++) {
const betweenText = normalizedText.substring(combination[i].end, combination[i + 1].start);
if (!ALLOWED_SEPARATORS_REGEX.test(betweenText)) {
isValid = false;
break;
}
}
if (isValid) {
const prevChar = normalizedText[overallStart - 1];
const nextChar = normalizedText[overallEnd];
const startBoundaryOK = !prevChar || !WORD_CHAR_REGEX.test(prevChar);
const endBoundaryOK = !nextChar || !WORD_CHAR_REGEX.test(nextChar);
if (startBoundaryOK && endBoundaryOK) {
const startMapping = textMap[overallStart];
const endMapping = textMap[overallEnd - 1];
if (startMapping && endMapping) {
return {
startNode: startMapping.node,
startOffset: startMapping.offset,
endNode: endMapping.node,
endOffset: endMapping.offset + 1
};
}
}
}
}
return null;
}
/**
* 预处理单个段落 DOM 节点,应用所有术语表规则并替换为占位符
*/
function _preprocessParagraph(p, preparedRules, placeholders, placeholderCache, engineName) {
const clone = p.cloneNode(true);
const { domRules, executionPlan } = preparedRules;
if (executionPlan && executionPlan.length > 0) {
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
if (node.parentElement.closest('[data-glossary-applied="true"]')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
const nodesToProcess = [];
let n;
while (n = walker.nextNode()) {
nodesToProcess.push(n);
}
while (nodesToProcess.length > 0) {
const currentNode = nodesToProcess.shift();
if (!currentNode.parentNode) continue;
const text = currentNode.nodeValue;
if (!text) continue;
let matchFound = false;
for (const planItem of executionPlan) {
const regex = planItem.regex || planItem.rule.regex;
regex.lastIndex = 0;
const match = regex.exec(text);
if (match) {
const fragment = document.createDocumentFragment();
const matchedText = match[0];
const matchIndex = match.index;
if (matchIndex > 0) {
fragment.appendChild(document.createTextNode(text.substring(0, matchIndex)));
}
let rule, finalValue;
if (planItem.type === 'combined') {
const captureIndex = match.slice(1).findIndex(m => m !== undefined);
rule = planItem.rules[captureIndex];
finalValue = rule.type === 'forbidden' ? matchedText : rule.replacement;
} else {
rule = planItem.rule;
if (rule.type === 'forbidden') {
finalValue = matchedText;
} else {
finalValue = matchedText.replace(regex, rule.replacement);
}
}
let placeholder;
if (placeholderCache.has(finalValue)) {
placeholder = placeholderCache.get(finalValue);
} else {
do {
placeholder = `ph_${generateRandomPlaceholder()}`;
} while (placeholders.has(placeholder));
placeholderCache.set(finalValue, placeholder);
placeholders.set(placeholder, { value: finalValue, rule: rule, originalHTML: matchedText });
}
fragment.appendChild(document.createTextNode(placeholder));
if (matchIndex + matchedText.length < text.length) {
fragment.appendChild(document.createTextNode(text.substring(matchIndex + matchedText.length)));
}
const newNodes = Array.from(fragment.childNodes).filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue);
if (newNodes.length > 0) {
nodesToProcess.unshift(...newNodes);
}
currentNode.parentNode.replaceChild(fragment, currentNode);
matchFound = true;
break;
}
}
}
}
if (domRules.length > 0) {
let domReplaced;
do {
domReplaced = false;
for (const rule of domRules) {
let match;
if (rule.isUnordered) {
match = findUnorderedDOMSequence(clone, rule);
} else {
match = findOrderedDOMSequence(clone, rule);
}
if (match) {
const range = document.createRange();
range.setStart(match.startNode, match.startOffset);
range.setEnd(match.endNode, match.endOffset);
const contents = range.extractContents();
const tempDiv = document.createElement('div');
tempDiv.appendChild(contents);
const originalHTML = tempDiv.innerHTML;
const finalValue = rule.type === 'forbidden' ? originalHTML : rule.replacement;
let placeholder;
if (placeholderCache.has(finalValue)) {
placeholder = placeholderCache.get(finalValue);
} else {
do {
placeholder = `ph_${generateRandomPlaceholder()}`;
} while (placeholders.has(placeholder));
placeholderCache.set(finalValue, placeholder);
placeholders.set(placeholder, { value: finalValue, rule: rule, originalHTML: originalHTML });
}
const placeholderNode = document.createTextNode(placeholder);
range.insertNode(placeholderNode);
clone.normalize();
domReplaced = true;
break;
}
}
} while (domReplaced);
}
return clone;
}
/**
* 替换一个 DOM 节点并完整保留所有 HTML 标签结构
*/
function replaceTextInNode(node, newText) {
if (node.nodeType === Node.TEXT_NODE) {
node.nodeValue = newText;
return;
}
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
const textNodes = [];
let currentNode;
while ((currentNode = walker.nextNode())) {
textNodes.push(currentNode);
}
if (textNodes.length > 0) {
textNodes[0].nodeValue = newText;
for (let i = 1; i < textNodes.length; i++) {
textNodes[i].nodeValue = '';
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
node.textContent = newText;
}
}
/**
* 译文后处理与占位符还原
*/
function _postprocessAndRestoreText(translatedText, placeholders) {
let processedText = translatedText;
try {
const fuzzyPlaceholderRegex = /(?:ph|p)[\s_\--_—::]*(\d{6})/gi;
processedText = processedText.replace(fuzzyPlaceholderRegex, (match, digits) => {
const standardPlaceholder = `ph_${digits}`;
if (placeholders.has(standardPlaceholder)) {
return standardPlaceholder;
}
return match;
});
} catch (e) {
Logger.warn('翻译', '占位符模糊还原出错', e);
}
if (placeholders.size === 0) {
return applyPostTranslationReplacements(processedText);
}
for (const [placeholder, data] of placeholders.entries()) {
const { value: replacement, originalHTML, rule } = data;
const escapedPlaceholder = placeholder.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const regex = new RegExp(escapedPlaceholder, 'g');
if (rule.matchStrategy === 'dom' && originalHTML) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = originalHTML;
const htmlChunks = Array.from(tempDiv.childNodes).filter(node =>
!(node.nodeType === Node.TEXT_NODE && !node.nodeValue.trim())
);
let finalHTML = '';
if (htmlChunks.length === 1) {
const singleChunk = htmlChunks[0];
replaceTextInNode(singleChunk, replacement);
finalHTML = singleChunk.nodeType === Node.ELEMENT_NODE ? singleChunk.outerHTML : singleChunk.nodeValue;
} else {
const separator = replacement.includes('·') || replacement.includes('・') ? /[·・]/ : /[\s--﹣—–]+/;
const joinSeparator = replacement.includes('·') || replacement.includes('・') ? '·' : ' ';
const translationParts = replacement.split(separator);
if (htmlChunks.length === translationParts.length) {
htmlChunks.forEach((chunk, index) => {
const part = translationParts[index];
replaceTextInNode(chunk, part);
});
finalHTML = htmlChunks.map(chunk => {
return chunk.nodeType === Node.ELEMENT_NODE ? chunk.outerHTML : chunk.nodeValue;
}).join(joinSeparator);
} else {
tempDiv.innerHTML = originalHTML;
tempDiv.textContent = replacement;
finalHTML = tempDiv.innerHTML;
}
}
processedText = processedText.replace(regex, finalHTML);
} else {
processedText = processedText.replace(regex, replacement);
}
}
return applyPostTranslationReplacements(processedText);
}
/**
* 特殊字符标准化工具
*/
const TextNormalizer = {
smallCapsMap: {
'ᴀ': 'A', 'ʙ': 'B', 'ᴄ': 'C', 'ᴅ': 'D', 'ᴇ': 'E', 'ғ': 'F', 'ɢ': 'G', 'ʜ': 'H', 'ɪ': 'I',
'ᴊ': 'J', 'ᴋ': 'K', 'ʟ': 'L', 'ᴍ': 'M', 'ɴ': 'N', 'ᴏ': 'O', 'ᴘ': 'P', 'ǫ': 'Q', 'ʀ': 'R',
'ꜱ': 'S', 'ᴛ': 'T', 'ᴜ': 'U', 'ᴠ': 'V', 'ᴡ': 'W', 'ʏ': 'Y', 'ᴢ': 'Z',
'Ɪ': 'I', 'Ɡ': 'G', 'Ʞ': 'K', 'Ɬ': 'L', 'Ɜ': 'E', 'Ꞷ': 'W'
},
regex: null,
init() {
if (this.regex) return;
const chars = Object.keys(this.smallCapsMap).join('');
this.regex = new RegExp(`[${chars}]`, 'g');
},
normalizeNode(rootNode) {
this.init();
const walker = document.createTreeWalker(
rootNode,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
const nodes = [];
while (node = walker.nextNode()) {
nodes.push(node);
}
nodes.forEach(textNode => {
const text = textNode.nodeValue;
if (!text) return;
if (this.regex.test(text)) {
this.regex.lastIndex = 0;
const newText = text.replace(this.regex, (char) => this.smallCapsMap[char] || char);
textNode.nodeValue = newText;
}
});
}
};
/**
* 段落翻译主函数
*/
async function translateParagraphs(paragraphs, { maxRetries = 3, isCancelled = () => false, knownFromLang = null, reqId = 'Unknown', skipRateLimit = false } = {}) {
const createCancellationError = () => {
const error = new Error('用户已取消翻译。');
error.type = 'user_cancelled';
error.noRetry = true;
return error;
};
if (isCancelled()) throw createCancellationError();
if (!paragraphs || paragraphs.length === 0) return new Map();
const indexedParagraphs = paragraphs.map((p, index) => ({
original: p,
index: index,
isSeparator: p.tagName === 'HR' || /^\s*[-—*~<>=.]{3,}\s*$/.test(p.textContent),
content: p.innerHTML
}));
const contentToTranslate = indexedParagraphs.filter(p => !p.isSeparator);
if (contentToTranslate.length === 0) {
const results = new Map();
indexedParagraphs.forEach(p => results.set(p.original, { status: 'success', content: p.content }));
return results;
}
const engineName = getValidEngineName();
const preparedRules = await getPreparedGlossaryRules();
const placeholders = new Map();
const placeholderCache = new Map();
const preprocessedParagraphs = [];
for (let i = 0; i < contentToTranslate.length; i++) {
if (isCancelled()) throw createCancellationError();
const p = contentToTranslate[i];
const processedNode = _preprocessParagraph(p.original, preparedRules, placeholders, placeholderCache, engineName);
TextNormalizer.normalizeNode(processedNode);
preprocessedParagraphs.push(processedNode);
}
Logger.info('翻译', `[${reqId}] 任务开始`, {
engine: engineName,
paragraphs: contentToTranslate.length,
placeholders: placeholders.size
});
for (let retryCount = 0; retryCount <= maxRetries; retryCount++) {
try {
let combinedTranslation = await requestRemoteTranslation(preprocessedParagraphs, {
isCancelled,
knownFromLang,
reqId,
skipRateLimit
});
combinedTranslation = combinedTranslation.replace(/[\u200B\u200C\u200D\uFEFF]/g, '');
const fuzzyPlaceholderRegex = /(?:ph|p)[\s\-_]*(\d{6})/gi;
const suspectedPlaceholders = [];
let match;
while ((match = fuzzyPlaceholderRegex.exec(combinedTranslation)) !== null) {
suspectedPlaceholders.push(`ph_${match[1]}`);
}
const legalPlaceholders = new Set(placeholders.keys());
const actualCounts = {};
legalPlaceholders.forEach(key => actualCounts[key] = 0);
let hasUnknownPlaceholders = false;
for (const suspected of suspectedPlaceholders) {
if (legalPlaceholders.has(suspected)) {
actualCounts[suspected]++;
} else {
hasUnknownPlaceholders = true;
}
}
let absoluteLossThreshold, proportionalLossThreshold, proportionalTriggerCount, catastrophicLossThreshold;
const defaults = CONFIG.SERVICE_CONFIG[engineName]?.VALIDATION || CONFIG.SERVICE_CONFIG.default.VALIDATION;
if (engineName === 'google_translate' || engineName === 'bing_translator') {
absoluteLossThreshold = defaults.absolute_loss;
proportionalLossThreshold = defaults.proportional_loss;
proportionalTriggerCount = defaults.proportional_trigger_count;
catastrophicLossThreshold = defaults.catastrophic_loss || 5;
} else {
const params = ProfileManager.getParamsByEngine(engineName);
const parts = (params.validation_thresholds || '').split(/[,,]/).map(s => parseFloat(s.trim()));
const isValid = parts.length >= 3 && !parts.slice(0, 3).some(isNaN);
absoluteLossThreshold = isValid ? parts[0] : defaults.absolute_loss;
proportionalLossThreshold = isValid ? parts[1] : defaults.proportional_loss;
proportionalTriggerCount = isValid ? parts[2] : defaults.proportional_trigger_count;
catastrophicLossThreshold = (isValid && !isNaN(parts[3])) ? parts[3] : (defaults.catastrophic_loss || 5);
}
let hasMissingPlaceholders = false;
let totalLoss = 0;
const expectedCounts = {};
const preprocessedText = preprocessedParagraphs.map(p => p.innerHTML).join(' ');
for (const key of placeholders.keys()) {
expectedCounts[key] = (preprocessedText.match(new RegExp(key, 'g')) || []).length;
}
for (const key of legalPlaceholders) {
const expected = expectedCounts[key];
const actual = actualCounts[key];
const loss = expected - actual;
if (loss > 0) {
totalLoss += loss;
const isCatastrophicLoss = expected > catastrophicLossThreshold && actual === 0;
const isAbsoluteLoss = loss >= absoluteLossThreshold;
const isProportionalLoss = expected >= proportionalTriggerCount && (loss / expected) >= proportionalLossThreshold;
if (isCatastrophicLoss || isAbsoluteLoss || isProportionalLoss) {
hasMissingPlaceholders = true;
break;
}
}
}
if (hasMissingPlaceholders || hasUnknownPlaceholders) {
const errorReason = hasUnknownPlaceholders ? "检测到未知占位符" : "占位符大量缺失";
Logger.warn('翻译', `[${reqId}] 占位符校验失败: ${errorReason}`, { totalLoss });
const err = new Error(`占位符校验失败 (${errorReason})`);
err.type = 'validation_failed';
throw err;
}
const restoredTranslation = _postprocessAndRestoreText(combinedTranslation, placeholders, engineName);
let translatedParts = [];
if (contentToTranslate.length === 1 && !restoredTranslation.trim().startsWith('1.')) {
translatedParts.push(restoredTranslation.trim());
} else {
const regex = /\d+\.\s*([\s\S]*?)(?=\n\d+\.|$)/g;
let match;
while ((match = regex.exec(restoredTranslation)) !== null) {
translatedParts.push(match[1].trim());
}
if (translatedParts.length !== contentToTranslate.length && restoredTranslation.includes('\n')) {
const potentialParts = restoredTranslation.split('\n').filter(p => p.trim().length > 0);
if (potentialParts.length === contentToTranslate.length) {
translatedParts = potentialParts.map(p => p.replace(/^\d+\.\s*/, '').trim());
}
}
}
if (translatedParts.length !== contentToTranslate.length) {
throw new Error('AI 响应格式不一致,分段数量不匹配');
}
const finalResults = new Map();
indexedParagraphs.forEach(p => {
if (p.isSeparator) {
finalResults.set(p.original, { status: 'success', content: p.content });
} else {
const originalPara = contentToTranslate.find(item => item.index === p.index);
if (originalPara) {
const transIndex = contentToTranslate.indexOf(originalPara);
const cleanedContent = AdvancedTranslationCleaner.clean(translatedParts[transIndex] || p.content);
finalResults.set(p.original, { status: 'success', content: cleanedContent });
}
}
});
return finalResults;
} catch (e) {
if (isCancelled() || e.type === 'user_cancelled') {
throw createCancellationError();
}
if (e.noRetry) {
Logger.error('翻译', `[${reqId}] 发生不可重试错误`, e.message);
throw e;
}
if (retryCount < maxRetries) {
let baseDelay = 5000;
if (e.type === 'rate_limit' || e.type === 'server_overloaded') {
baseDelay = 10000;
} else if (e.type === 'validation_failed') {
baseDelay = 2000;
}
const delay = baseDelay * Math.pow(2, retryCount);
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
const finalDelay = Math.floor(delay + jitter);
Logger.warn('翻译', `[${reqId}] 任务重试 (${retryCount + 1}/${maxRetries})`, {
reason: e.message,
waitingFor: `${Math.round(finalDelay / 1000)}s`
});
await sleep(finalDelay);
if (isCancelled()) throw createCancellationError();
continue;
}
Logger.error('翻译', `[${reqId}] 重试次数耗尽,任务失败: ${e.message}`);
if (e.message.includes('分段数量不匹配') && paragraphs.length > 1) {
Logger.warn('翻译', `[${reqId}] 触发逐段回退策略`);
if (isCancelled()) throw createCancellationError();
const fallbackResults = new Map();
for (const p of paragraphs) {
if (isCancelled()) break;
const singleResultMap = await translateParagraphs([p], {
maxRetries: 1,
isCancelled,
knownFromLang,
reqId: `${reqId}-FB`,
skipRateLimit
});
const singleResult = singleResultMap.get(p);
fallbackResults.set(p, singleResult || { status: 'error', content: '逐段翻译失败' });
}
return fallbackResults;
}
const finalResults = new Map();
indexedParagraphs.forEach(p => {
if (p.isSeparator) {
finalResults.set(p.original, { status: 'success', content: p.content });
} else {
finalResults.set(p.original, { status: 'error', content: `翻译失败:${e.message}` });
}
});
return finalResults;
}
}
}
/**
* 通用翻译控制器基座:管理状态切换、RunID 校验及 UI 更新
*/
function createBaseController(config) {
const { buttonWrapper, originalButtonText, onStart, onPause, onClear } = config;
const controller = {
state: 'idle',
currentRunId: 0,
updateButtonState: function (text, stateClass = '') {
if (buttonWrapper) {
const button = buttonWrapper.querySelector('div');
if (button) button.textContent = text;
buttonWrapper.className = `translate-me-ao3-wrapper ${stateClass}`;
}
},
getCancelChecker: function (runId) {
return () => this.state !== 'running' || this.currentRunId !== runId;
},
start: async function () {
if (this.state === 'running') return;
this.currentRunId++;
const myRunId = this.currentRunId;
this.state = 'running';
this.updateButtonState('翻译中…', 'state-running');
await sleep(10);
const isCancelled = this.getCancelChecker(myRunId);
const onDone = () => {
if (!isCancelled()) {
this.state = 'complete';
this.updateButtonState('清除译文', 'state-complete');
}
};
await onStart(isCancelled, onDone);
},
pause: function () {
if (this.state !== 'running') return;
this.state = 'paused';
this.currentRunId++;
if (onPause) onPause();
this.updateButtonState('暂停中…', 'state-paused');
},
resume: function () {
if (this.state !== 'paused') return;
this.start();
},
clear: function () {
this.state = 'idle';
this.currentRunId++;
if (onPause) onPause();
onClear();
this.updateButtonState(originalButtonText, 'state-idle');
},
handleClick: function () {
switch (this.state) {
case 'idle':
this.start();
break;
case 'running':
this.pause();
break;
case 'paused':
this.resume();
break;
case 'complete':
this.clear();
break;
}
}
};
return controller;
}
/**
* 块级文本翻译控制器(支持正文、评论、动态等)
*/
function createTranslationController(options) {
const { containerElement, buttonWrapper, originalButtonText, isLazyLoad } = options;
let activeTask = null;
let translatedTitleElement = null;
const controller = createBaseController({
buttonWrapper,
originalButtonText,
onStart: async (isCancelled, onDone) => {
let titleNode = null;
let titleTempDiv = null;
let isChapterTitle = false;
let prefaceGroup = containerElement.previousElementSibling;
while (prefaceGroup && !prefaceGroup.classList.contains('preface')) {
prefaceGroup = prefaceGroup.previousElementSibling;
}
if (!prefaceGroup && containerElement.parentElement.classList.contains('chapter')) {
prefaceGroup = containerElement.parentElement.querySelector('.chapter.preface.group');
}
if (prefaceGroup) {
const titleEl = prefaceGroup.querySelector('h3.title');
if (titleEl && !titleEl.nextElementSibling?.classList.contains('translated-title-element')) {
const fullText = titleEl.textContent.trim();
const simpleChapterRegex = /^(?:Chapter|第)\s*\d+\s*(?:章)?$/i;
const link = titleEl.querySelector('a');
let textToTranslate = fullText;
let isValid = true;
if (link) {
const linkText = link.textContent;
const remaining = fullText.replace(linkText, '').trim();
if (!remaining || /^[::]\s*$/.test(remaining)) {
isValid = false;
} else {
textToTranslate = remaining.replace(/^[::]\s*/, '');
}
} else {
if (simpleChapterRegex.test(fullText)) {
isValid = false;
}
}
if (isValid && textToTranslate) {
titleNode = titleEl;
titleTempDiv = document.createElement('div');
titleTempDiv.textContent = textToTranslate;
isChapterTitle = true;
}
}
}
if (!titleNode) {
const workPreface = containerElement.closest('.preface.group');
if (workPreface) {
const workTitleEl = workPreface.querySelector('h2.title.heading');
if (workTitleEl && !workTitleEl.nextElementSibling?.classList.contains('translated-title-element')) {
const clone = workTitleEl.cloneNode(true);
clone.querySelectorAll('img, svg').forEach(el => el.remove());
const textContent = clone.textContent.trim();
if (textContent) {
titleNode = workTitleEl;
titleTempDiv = document.createElement('div');
titleTempDiv.textContent = textContent;
isChapterTitle = false;
}
}
}
}
const customRenderers = new Map();
if (titleTempDiv) {
customRenderers.set(titleTempDiv, (_node, result) => {
if (result.status === 'success') {
const translatedTitle = titleNode.cloneNode(true);
translatedTitle.classList.add('translated-title-element');
translatedTitle.removeAttribute('id');
if (isChapterTitle) {
const link = translatedTitle.querySelector('a');
if (link) {
let next = link.nextSibling;
while (next) {
const toRemove = next;
next = next.nextSibling;
toRemove.remove();
}
translatedTitle.appendChild(document.createTextNode(`: ${result.content}`));
} else {
translatedTitle.textContent = result.content;
}
} else {
let textNodeFound = false;
Array.from(translatedTitle.childNodes).forEach(child => {
if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim()) {
if (!textNodeFound) {
child.nodeValue = ` ${result.content} `;
textNodeFound = true;
} else {
child.nodeValue = '';
}
}
});
if (!textNodeFound) {
translatedTitle.appendChild(document.createTextNode(result.content));
}
}
titleNode.after(translatedTitle);
translatedTitleElement = translatedTitle;
const currentMode = GM_getValue('translation_display_mode', 'bilingual');
if (currentMode === 'translation_only') {
titleNode.style.display = 'none';
}
}
});
}
const instanceState = {
elementState: new WeakMap(),
isFirstTranslationChunk: true,
};
activeTask = runUniversalTranslationEngine({
containerElement,
isCancelled,
onComplete: onDone,
instanceState,
useObserver: isLazyLoad,
prependNodes: titleTempDiv ? [titleTempDiv] : [],
customRenderers: customRenderers,
onRetry: () => {
const failedUnits = Array.from(containerElement.querySelectorAll('[data-translation-state="error"]'));
if (failedUnits.length === 0) return;
failedUnits.forEach(unit => {
const errorNode = unit.nextElementSibling;
if (errorNode && errorNode.classList.contains('translated-by-ao3-translator-error')) {
errorNode.remove();
}
delete unit.dataset.translationState;
});
if (controller.state === 'complete') {
controller.state = 'running';
controller.updateButtonState('翻译中…', 'state-running');
}
if (activeTask) {
activeTask.addUnits(failedUnits);
activeTask.scheduleProcessing(true);
}
}
});
},
onPause: () => {
if (activeTask && activeTask.disconnect) {
activeTask.disconnect();
}
activeTask = null;
containerElement.querySelectorAll('[data-translation-state="translating"]').forEach(unit => {
delete unit.dataset.translationState;
});
},
onClear: () => {
const internalNodes = containerElement.querySelectorAll('.translated-by-ao3-translator, .translated-by-ao3-translator-error');
internalNodes.forEach(node => node.remove());
let nextNode = containerElement.nextElementSibling;
while (nextNode && nextNode !== buttonWrapper) {
if (nextNode.classList.contains('translated-by-ao3-translator') || nextNode.classList.contains('translated-by-ao3-translator-error')) {
const nodeToRemove = nextNode;
nextNode = nextNode.nextElementSibling;
nodeToRemove.remove();
} else {
nextNode = nextNode.nextElementSibling;
}
}
containerElement.querySelectorAll('[data-translation-state]').forEach(unit => {
unit.style.display = '';
delete unit.dataset.translationState;
});
if (containerElement.dataset.translationState) {
containerElement.style.display = '';
delete containerElement.dataset.translationState;
}
if (translatedTitleElement) {
const originalTitle = translatedTitleElement.previousElementSibling;
if (originalTitle) originalTitle.style.display = '';
translatedTitleElement.remove();
translatedTitleElement = null;
}
}
});
return controller;
}
/**
* 标签区域翻译控制器
*/
function createTagsTranslationController(options) {
const { containerElement, buttonWrapper, originalButtonText } = options;
let errorElement = null;
const controller = createBaseController({
buttonWrapper,
originalButtonText,
onStart: async (isCancelled, onDone) => {
try {
await runTagsTranslationEngine(containerElement, isCancelled);
if (isCancelled()) return;
onDone();
} catch (error) {
if (isCancelled()) return;
onDone();
const errorDiv = document.createElement('div');
errorDiv.className = 'translated-by-ao3-translator-error';
errorDiv.style.margin = '15px 0';
errorDiv.innerHTML = `翻译失败:${error.message}`;
const retryBtn = document.createElement('span');
retryBtn.className = 'retry-translation-button';
retryBtn.title = '重试';
retryBtn.innerHTML = ``;
retryBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (errorElement) {
errorElement.remove();
errorElement = null;
}
controller.start();
});
errorDiv.appendChild(retryBtn);
buttonWrapper.before(errorDiv);
errorElement = errorDiv;
Logger.error('翻译', '标签翻译失败', error);
}
},
onClear: () => {
const translations = containerElement.querySelectorAll('.ao3-tag-translation');
translations.forEach(el => el.remove());
const originals = containerElement.querySelectorAll('.ao3-tag-original');
originals.forEach(el => el.style.display = '');
if (errorElement) {
errorElement.remove();
errorElement = null;
}
containerElement.style.display = '';
}
});
return controller;
}
/**
* 混合作品卡片(Blurb)翻译控制器,同步管理简介与标签的翻译任务
*/
function createBlurbTranslationController(options) {
const { summaryElement, tagsElement, buttonWrapper, originalButtonText } = options;
let errorElement = null;
let activeSummaryTask = null;
const controller = createBaseController({
buttonWrapper,
originalButtonText,
onStart: async (isCancelled, onDone) => {
try {
const tagsPromise = runTagsTranslationEngine(tagsElement, isCancelled);
const summaryPromise = new Promise((resolve) => {
const instanceState = {
elementState: new WeakMap(),
isFirstTranslationChunk: true,
};
activeSummaryTask = runUniversalTranslationEngine({
containerElement: summaryElement,
isCancelled,
onComplete: resolve,
instanceState,
useObserver: false,
onRetry: () => {
const failedUnits = Array.from(summaryElement.querySelectorAll('[data-translation-state="error"]'));
if (failedUnits.length === 0) return;
failedUnits.forEach(unit => {
const errorNode = unit.nextElementSibling;
if (errorNode && errorNode.classList.contains('translated-by-ao3-translator-error')) {
errorNode.remove();
}
delete unit.dataset.translationState;
});
if (controller.state === 'complete') {
controller.state = 'running';
controller.updateButtonState('翻译中…', 'state-running');
}
if (activeSummaryTask) {
activeSummaryTask.addUnits(failedUnits);
activeSummaryTask.scheduleProcessing(true);
}
}
});
});
await Promise.all([tagsPromise, summaryPromise]);
if (isCancelled()) return;
onDone();
} catch (error) {
if (isCancelled()) return;
onDone();
const errorDiv = document.createElement('div');
errorDiv.className = 'translated-by-ao3-translator-error';
errorDiv.style.margin = '15px 0';
errorDiv.innerHTML = `翻译失败:${error.message}`;
const retryBtn = document.createElement('span');
retryBtn.className = 'retry-translation-button';
retryBtn.title = '重试';
retryBtn.innerHTML = ``;
retryBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (errorElement) {
errorElement.remove();
errorElement = null;
}
controller.start();
});
errorDiv.appendChild(retryBtn);
buttonWrapper.before(errorDiv);
errorElement = errorDiv;
Logger.error('翻译', 'Blurb 翻译失败', error);
}
},
onPause: () => {
if (activeSummaryTask && activeSummaryTask.disconnect) {
activeSummaryTask.disconnect();
}
activeSummaryTask = null;
summaryElement.querySelectorAll('[data-translation-state="translating"]').forEach(unit => {
delete unit.dataset.translationState;
});
},
onClear: () => {
const internalTranslationNodes = summaryElement.querySelectorAll('.translated-by-ao3-translator, .translated-by-ao3-translator-error');
internalTranslationNodes.forEach(node => node.remove());
let nextNode = summaryElement.nextSibling;
while (nextNode && (nextNode.classList?.contains('translated-by-ao3-translator') || nextNode.classList?.contains('translated-by-ao3-translator-error'))) {
const nodeToRemove = nextNode;
nextNode = nextNode.nextSibling;
nodeToRemove.remove();
}
summaryElement.querySelectorAll('[data-translation-state]').forEach(unit => {
unit.style.display = '';
delete unit.dataset.translationState;
});
const translations = tagsElement.querySelectorAll('.ao3-tag-translation');
translations.forEach(el => el.remove());
const originals = tagsElement.querySelectorAll('.ao3-tag-original');
originals.forEach(el => el.style.display = '');
if (errorElement) {
errorElement.remove();
errorElement = null;
}
tagsElement.style.display = '';
if (tagsElement.parentElement.classList.contains('wrapper') && tagsElement.tagName === 'DL') {
tagsElement.parentElement.style.display = '';
}
}
});
return controller;
}
/**
* 标签区域翻译引擎
*/
async function runTagsTranslationEngine(containerElement, isCancelled) {
if (isCancelled()) return null;
const targetSelectors = [
'dd.fandom a.tag', 'dd.relationship a.tag', 'dd.character a.tag', 'dd.freeform a.tag',
'dd.series a:not(.previous):not(.next)', // <--- 修改了这一行
'dd.collections a', 'dd.language', 'li.fandoms a.tag',
'li.relationships a.tag', 'li.characters a.tag', 'li.freeforms a.tag',
'a.tag:not(.rating):not(.warning):not(.category)'
];
const nodesToTranslate = [];
const wrapperMap = new Map();
const processedElements = new Set();
targetSelectors.forEach(selector => {
containerElement.querySelectorAll(selector).forEach(el => {
if (processedElements.has(el)) return;
processedElements.add(el);
if (el.closest('.rating, .warnings, .category, .warning')) return;
if (el.querySelector('.ao3-tag-translation')) return;
const text = el.textContent.trim();
const fullDictionary = {
...pageConfig.staticDict,
...pageConfig.globalFlexibleDict,
...pageConfig.pageFlexibleDict
};
if (text && !/^\d+$/.test(text) && !fullDictionary[text]) {
let originalSpan = el.querySelector('.ao3-tag-original');
if (!originalSpan) {
originalSpan = document.createElement('span');
originalSpan.className = 'ao3-tag-original';
while (el.firstChild) {
originalSpan.appendChild(el.firstChild);
}
el.appendChild(originalSpan);
}
nodesToTranslate.push(originalSpan);
wrapperMap.set(originalSpan, el);
}
});
});
if (nodesToTranslate.length > 0) {
try {
const reqId = 'Tags-' + Math.random().toString(36).substring(2, 6).toUpperCase();
const translationResults = await translateParagraphs(nodesToTranslate, { isCancelled, reqId });
if (isCancelled()) return null;
nodesToTranslate.forEach(originalSpan => {
const result = translationResults.get(originalSpan);
const parentLink = wrapperMap.get(originalSpan);
if (result && result.status === 'success' && parentLink) {
if (parentLink.querySelector('.ao3-tag-translation')) return;
const translationSpan = document.createElement('span');
translationSpan.className = 'ao3-tag-translation';
const cleanedContent = result.content.trim().replace(/[。\.]$/, '');
translationSpan.textContent = cleanedContent;
parentLink.appendChild(translationSpan);
}
});
} catch (error) {
if (isCancelled()) return null;
throw error;
}
}
const currentMode = GM_getValue('translation_display_mode', 'bilingual');
applyDisplayModeChange(currentMode);
return containerElement;
}
/**
* DOM 处理与遍历器
*/
class DOMNormalizer {
constructor() {
this.elementState = new WeakMap();
this.splitThreshold = 200;
this.yieldInterval = 30;
}
async *generateUnits(container) {
const splitSelectors = 'p, blockquote';
const elementsToSplit = Array.from(container.querySelectorAll(splitSelectors))
.filter(el => !this._isTranslated(el));
for (let i = 0; i < elementsToSplit.length; i++) {
const el = elementsToSplit[i];
if (!this.elementState.has(el) && el.textContent.length > this.splitThreshold && el.querySelector('br')) {
const newElements = this._splitElement(el);
if (newElements.length > 0) el.replaceWith(...newElements);
} else {
this.elementState.set(el, { preprocessed: true });
}
if ((i + 1) % this.yieldInterval === 0) await sleep(0);
}
const liElements = Array.from(container.querySelectorAll('li'));
for (let i = 0; i < liElements.length; i++) {
this._wrapListContent(liElements[i]);
if ((i + 1) % this.yieldInterval === 0) await sleep(0);
}
const selectors = 'p, blockquote, li, h1, h2, h3, h4, h5, h6, hr, center';
const skipHeaders = ['Summary', 'Notes', 'Work Text', 'Chapter Text'];
const candidates = Array.from(container.querySelectorAll(selectors))
.filter(el => !this._isTranslated(el));
let yieldedAny = false;
for (let i = 0; i < candidates.length; i++) {
const unit = candidates[i];
const content = unit.textContent.trim();
if (!content && unit.tagName !== 'HR') continue;
if (skipHeaders.includes(content)) continue;
if (unit.querySelector(selectors)) continue;
yieldedAny = true;
yield unit;
if ((i + 1) % this.yieldInterval === 0) await sleep(0);
}
if (!yieldedAny && container.textContent.trim().length > 0) {
yield container;
}
}
_isTranslated(el) {
return el.closest('.translated-by-ao3-translator, .translated-by-ao3-translator-error');
}
_splitElement(el) {
const newElements = [];
let contentBuffer = [];
const flushBuffer = () => {
if (contentBuffer.length === 0) return;
const hasContent = contentBuffer.some(node =>
(node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) ||
(node.nodeType === Node.ELEMENT_NODE)
);
if (hasContent) {
const newEl = document.createElement(el.tagName);
if (el.className) newEl.className = el.className;
contentBuffer.forEach(node => newEl.appendChild(node));
this.elementState.set(newEl, { preprocessed: true });
newElements.push(newEl);
}
contentBuffer = [];
};
Array.from(el.childNodes).forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') flushBuffer();
else contentBuffer.push(node);
});
flushBuffer();
return newElements;
}
_wrapListContent(li) {
if (li.querySelector('ul, ol')) {
const childNodes = Array.from(li.childNodes);
let contentBuffer = [];
const flushBuffer = () => {
if (contentBuffer.length === 0) return;
if (contentBuffer.some(n => (n.nodeType === Node.TEXT_NODE && n.nodeValue.trim().length > 0) || (n.nodeType === Node.ELEMENT_NODE && n.textContent.trim().length > 0))) {
const p = document.createElement('p');
p.style.margin = '0'; p.style.padding = '0'; p.style.display = 'inline-block'; p.style.width = '100%';
contentBuffer[0].parentNode.insertBefore(p, contentBuffer[0]);
contentBuffer.forEach(n => p.appendChild(n));
this.elementState.set(p, { preprocessed: true });
}
contentBuffer = [];
};
childNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'UL' || node.tagName === 'OL')) flushBuffer();
else contentBuffer.push(node);
});
flushBuffer();
}
}
}
/**
* 优先级队列管理器
*/
class PriorityQueueManager {
constructor(prependNodes = []) {
this.queue = new Set();
this.prependSet = new Set(prependNodes);
prependNodes.forEach(n => this.queue.add(n));
}
add(unit, delay = 0) {
if (!unit.dataset.translationState) {
if (delay > 0) {
unit.dataset.readyAt = Date.now() + delay;
} else {
delete unit.dataset.readyAt;
}
this.queue.add(unit);
}
}
remove(unit) {
this.queue.delete(unit);
delete unit.dataset.readyAt;
}
get size() {
return this.queue.size;
}
getSortedList() {
if (this.queue.size === 0) return [];
const high = [];
const medium = [];
const low = [];
const viewportHeight = window.innerHeight;
const now = Date.now();
for (const unit of this.queue) {
if (unit.dataset.readyAt) {
const readyAt = parseInt(unit.dataset.readyAt, 10);
if (now < readyAt) {
continue;
}
}
if (this.prependSet.has(unit)) {
high.push(unit);
continue;
}
const rect = unit.getBoundingClientRect();
const isVisible = (rect.top < viewportHeight && rect.bottom >= 0);
if (isVisible) {
medium.push(unit);
} else {
low.push(unit);
}
}
return [...high, ...medium, ...low];
}
}
/**
* 分包策略
*/
class BatchStrategy {
constructor(configProvider) {
this.config = configProvider;
}
createBatch(sortedList) {
if (sortedList.length === 0) return { batch: [], reason: 'empty' };
const { chunkSize, paragraphLimit } = this.config.getLimits();
const batch = [];
let currentChars = 0;
let reason = 'underfilled';
for (const unit of sortedList) {
const isSeparator = unit.tagName === 'HR' || /^\s*[-—*~<>=.]{3,}\s*$/.test(unit.textContent);
if (isSeparator) {
if (batch.length > 0) {
reason = 'separator_cut';
break;
} else {
batch.push(unit);
reason = 'separator_single';
break;
}
}
batch.push(unit);
currentChars += unit.textContent.length;
if (batch.length >= paragraphLimit || currentChars >= chunkSize) {
reason = 'full';
break;
}
}
return { batch, reason };
}
}
/**
* 渲染代理
*/
class RenderDelegate {
constructor(customRenderersMap, displayModeGetter) {
this.customRenderers = customRenderersMap || new Map();
this.getDisplayMode = displayModeGetter;
}
applyResult(unit, result, onRetry) {
if (this.customRenderers.has(unit)) {
try {
const renderer = this.customRenderers.get(unit);
renderer(unit, result);
unit.dataset.translationState = 'translated';
} catch (e) {
Logger.error('翻译', '自定义渲染失败', e);
unit.dataset.translationState = 'error';
}
return;
}
const transNode = document.createElement('div');
const newTranslatedElement = unit.cloneNode(false);
newTranslatedElement.removeAttribute('data-translation-state');
if (result.status === 'success') {
delete unit.dataset.batchRetryCount;
transNode.className = 'translated-by-ao3-translator';
newTranslatedElement.innerHTML = result.content;
if (this.getDisplayMode() === 'translation_only') {
unit.style.display = 'none';
}
unit.dataset.translationState = 'translated';
} else {
transNode.className = 'translated-by-ao3-translator-error';
newTranslatedElement.innerHTML = `翻译失败:${result.content.replace('翻译失败:', '')}`;
unit.dataset.translationState = 'error';
const retryBtn = this._createRetryButton(unit, onRetry);
newTranslatedElement.appendChild(retryBtn);
}
transNode.appendChild(newTranslatedElement);
unit.after(transNode);
}
_createRetryButton(_unit, onRetry) {
const span = document.createElement('span');
span.className = 'retry-translation-button';
span.title = '重试';
span.innerHTML = ``;
span.addEventListener('click', (e) => {
e.stopPropagation();
onRetry();
});
return span;
}
}
/**
* 资源管理器
*/
class ResourceManager {
constructor(engineName, maxConcurrency = 5) {
this.engineName = engineName;
this.maxConcurrency = maxConcurrency;
this.activeCount = 0;
this.tokenBucket = new GlobalTokenBucket();
}
canSchedule() {
return this.activeCount < this.maxConcurrency;
}
async acquireToken() {
if (!this.canSchedule()) {
return { success: false, waitTime: 1000 };
}
const result = await this.tokenBucket.consume(1, this.engineName);
if (result.success) {
this.activeCount++;
}
return result;
}
release() {
this.activeCount = Math.max(0, this.activeCount - 1);
}
reportError(error) {
if (error.type === 'rate_limit' || error.type === 'server_overloaded' || (error.message && (error.message.includes('429') || error.message.includes('503')))) {
const freezeTime = 10000;
this.tokenBucket.triggerFreeze(freezeTime);
}
}
}
/**
* 通用翻译调度引擎
*/
class UniversalEngine {
constructor(options) {
this.container = options.containerElement;
this.isCancelled = options.isCancelled;
this.onComplete = options.onComplete;
this.onProgress = options.onProgress;
this.onRetryCallback = options.onRetry;
this.normalizer = new DOMNormalizer();
this.queueManager = new PriorityQueueManager(options.prependNodes);
const engine = getValidEngineName();
this.batchStrategy = new BatchStrategy({
getLimits: () => {
const isSpecial = ['google_translate', 'bing_translator'].includes(engine);
const base = CONFIG.SERVICE_CONFIG[engine] || CONFIG.SERVICE_CONFIG.default;
if (isSpecial) {
return {
chunkSize: base.CHUNK_SIZE,
paragraphLimit: base.PARAGRAPH_LIMIT
};
}
const params = ProfileManager.getParamsByEngine(engine);
return {
chunkSize: params.chunk_size,
paragraphLimit: params.para_limit
};
}
});
this.renderer = new RenderDelegate(options.customRenderers, () => GM_getValue('translation_display_mode'));
this.resourceManager = new ResourceManager(engine);
this.observer = null;
this.timer = null;
this.detectedLang = null;
this.totalUnits = 0;
this.processedUnits = 0;
this.prependNodes = options.prependNodes || [];
this.hasSkippedFirstLimit = false;
}
async start(useObserver = true) {
const unitGenerator = this.normalizer.generateUnits(this.container);
if (useObserver) {
const rootMargin = this._getRootMargin();
this.observer = new IntersectionObserver((entries) => {
if (this.isCancelled()) return;
let added = false;
entries.forEach(entry => {
if (entry.isIntersecting && !entry.target.dataset.translationState) {
this.queueManager.add(entry.target);
added = true;
}
});
if (added) this.schedule(false);
}, { rootMargin });
}
let sampleBatch = [];
let detectionAttempted = false;
for await (const unit of unitGenerator) {
if (this.isCancelled()) break;
if (unit.dataset.translationState) continue;
this.totalUnits++;
if (!this.detectedLang && !detectionAttempted && !this.prependNodes.includes(unit)) {
sampleBatch.push(unit);
if (sampleBatch.length >= 5) {
await this._detectLanguage(sampleBatch);
detectionAttempted = true;
sampleBatch = [];
}
}
if (useObserver) {
this.observer.observe(unit);
if (this._isInViewport(unit)) this.queueManager.add(unit);
} else {
this.queueManager.add(unit);
}
if (this.totalUnits <= 10 || this.totalUnits % 50 === 0) {
this.schedule(this.totalUnits <= 10);
}
}
if (!this.detectedLang && !detectionAttempted && sampleBatch.length > 0) {
await this._detectLanguage(sampleBatch);
}
if (!useObserver) {
this.schedule(true);
}
if (this.totalUnits === 0) {
this._finish();
}
}
schedule(force = false) {
if (this.isCancelled()) return;
clearTimeout(this.timer);
this.timer = setTimeout(() => this.runLoop(force), 100);
}
async runLoop(force) {
if (this.isCancelled()) return;
if (!this.resourceManager.canSchedule()) return;
if (this.queueManager.size === 0) {
if (this.processedUnits >= this.totalUnits) this._finish();
return;
}
const sortedList = this.queueManager.getSortedList();
const { batch, reason } = this.batchStrategy.createBatch(sortedList);
if (batch.length === 0) {
if (this.queueManager.size > 0) {
this.timer = setTimeout(() => this.schedule(true), 1000);
}
return;
}
const isFull = reason === 'full';
const isSeparatorAction = reason.startsWith('separator');
if (!force && !isFull && !isSeparatorAction) {
this.timer = setTimeout(() => this.schedule(true), 4000);
return;
}
const tokenResult = await this.resourceManager.acquireToken();
if (!tokenResult.success) {
setTimeout(() => this.schedule(force), tokenResult.waitTime + 50);
return;
}
this._executeBatch(batch);
if (this.resourceManager.canSchedule() && this.queueManager.size > 0) {
this.schedule(false);
}
}
async _executeBatch(batch) {
batch.forEach(el => {
this.queueManager.remove(el);
if (this.observer) this.observer.unobserve(el);
el.dataset.translationState = 'translating';
});
try {
const validUnits = batch.filter(el => el.tagName !== 'HR' && el.textContent.trim());
const reqId = `Batch-${Math.random().toString(36).substring(2, 6).toUpperCase()}`;
let results = new Map();
if (validUnits.length > 0) {
const isFirstBatch = !this.hasSkippedFirstLimit;
if (isFirstBatch) {
this.hasSkippedFirstLimit = true;
}
results = await translateParagraphs(validUnits, {
isCancelled: this.isCancelled,
knownFromLang: this.detectedLang,
reqId: reqId,
maxRetries: 3,
skipRateLimit: isFirstBatch
});
}
batch.forEach(el => {
if (el.dataset.translationState !== 'translating') {
return;
}
this.processedUnits++;
if (el.tagName === 'HR') {
el.dataset.translationState = 'translated';
return;
}
const res = results.get(el) || { status: 'error', content: 'Unknown error' };
this.renderer.applyResult(el, res, this.onRetryCallback);
});
} catch (e) {
if (!this.isCancelled() && e.type !== 'user_cancelled') {
Logger.error('翻译', '批次执行发生未捕获错误', e);
batch.forEach(el => {
if (el.dataset.translationState !== 'translating') {
return;
}
this.processedUnits++;
this.renderer.applyResult(el, { status: 'error', content: e.message }, this.onRetryCallback);
});
}
} finally {
this.resourceManager.release();
if (this.onProgress) this.onProgress(this.processedUnits, this.totalUnits);
this.schedule(false);
}
}
async _detectLanguage(samples) {
const userSelectedFromLang = GM_getValue('from_lang', 'auto');
if (userSelectedFromLang === 'script_auto') {
const textToDetect = samples.map(p => p.textContent).join(' ').substring(0, 200);
this.detectedLang = await LanguageDetectService.detect(textToDetect);
Logger.info('翻译', `自动检测源语言: ${this.detectedLang}`);
} else {
this.detectedLang = userSelectedFromLang;
}
}
_getRootMargin() {
const engineName = getValidEngineName();
const isSpecial = ['google_translate', 'bing_translator'].includes(engineName);
const base = CONFIG.SERVICE_CONFIG[engineName] || CONFIG.SERVICE_CONFIG.default;
if (isSpecial) {
return base.LAZY_LOAD_ROOT_MARGIN;
}
const params = ProfileManager.getParamsByEngine(engineName);
return params.lazy_load_margin;
}
_isInViewport(el) {
const rect = el.getBoundingClientRect();
return (rect.top < window.innerHeight && rect.bottom >= 0);
}
_finish() {
if (this.observer) this.observer.disconnect();
if (this.onComplete) this.onComplete();
}
disconnect() {
if (this.observer) this.observer.disconnect();
}
addUnits(units) {
units.forEach(u => this.queueManager.add(u));
this.processedUnits = Math.max(0, this.processedUnits - units.length);
}
scheduleProcessing(force) {
this.schedule(force);
}
}
/**
* 通用翻译引擎启动入口
*/
function runUniversalTranslationEngine(options) {
const engine = new UniversalEngine(options);
const useObserver = options.useObserver !== false;
engine.start(useObserver);
return {
disconnect: () => engine.disconnect(),
addUnits: (units) => engine.addUnits(units),
scheduleProcessing: (force) => engine.scheduleProcessing(force)
};
}
/**
* 各种术语表变量
*/
const CUSTOM_GLOSSARIES_KEY = 'ao3_custom_glossaries';
const IMPORTED_GLOSSARY_KEY = 'ao3_imported_glossary';
const GLOSSARY_METADATA_KEY = 'ao3_glossary_metadata';
const ONLINE_GLOSSARY_ORDER_KEY = 'ao3_online_glossary_order';
const POST_REPLACE_STRING_KEY = 'ao3_post_replace_string';
const POST_REPLACE_MAP_KEY = 'ao3_post_replace_map';
const POST_REPLACE_RULES_KEY = 'ao3_post_replace_rules';
const POST_REPLACE_SELECTED_ID_KEY = 'ao3_post_replace_selected_id';
const POST_REPLACE_EDIT_MODE_KEY = 'ao3_post_replace_edit_mode';
const LAST_SELECTED_GLOSSARY_KEY = 'ao3_last_selected_glossary_url';
const GLOSSARY_RULES_CACHE_KEY = 'ao3_glossary_rules_cache';
const GLOSSARY_STATE_VERSION_KEY = 'ao3_glossary_state_version';
/**
* 解析自定义的、非 JSON 格式的术语表文本
*/
function parseCustomGlossaryFormat(text) {
const result = {
metadata: {},
terms: {},
generalTerms: {},
multiPartTerms: {},
multiPartGeneralTerms: {},
forbiddenTerms: [],
regexTerms: []
};
const lines = text.split('\n');
const sectionHeaders = {
TERMS: ['terms', '词条'],
GENERAL_TERMS: ['general terms', '通用词条'],
FORBIDDEN_TERMS: ['forbidden terms', '禁翻词条'],
REGEX_TERMS: ['regex', '正则表达式']
};
const sections = [];
let metadataLines = [];
let inMetadata = true;
for (let i = 0; i < lines.length; i++) {
const trimmedLine = lines[i].trim().toLowerCase().replace(/[::\s]*$/, '');
let isHeader = false;
for (const key in sectionHeaders) {
if (sectionHeaders[key].includes(trimmedLine)) {
sections.push({ type: key, start: i + 1 });
isHeader = true;
inMetadata = false;
break;
}
}
if (inMetadata && lines[i].trim()) {
metadataLines.push(lines[i]);
}
}
const metadataRegex = /^\s*(maintainer|version|last_updated|维护者|版本号|更新时间)\s*[::]\s*(.*?)\s*[,,]?\s*$/;
for (const line of metadataLines) {
const metadataMatch = line.match(metadataRegex);
if (metadataMatch) {
let key = metadataMatch[1].trim();
let value = metadataMatch[2].trim();
const keyMap = { '维护者': 'maintainer', '版本号': 'version', '更新时间': 'last_updated' };
result.metadata[keyMap[key] || key] = value;
}
}
const processLine = (line, target, multiPartTarget) => {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('//')) return;
const multiPartParts = trimmedLine.split(/[==]/, 2);
if (multiPartParts.length === 2) {
const key = multiPartParts[0].trim();
const value = multiPartParts[1].trim().replace(/[,,]$/, '');
if (key && value) multiPartTarget[key] = value;
return;
}
const singleParts = trimmedLine.split(/[::]/, 2);
if (singleParts.length === 2) {
const key = singleParts[0].trim();
const value = singleParts[1].trim().replace(/[,,]$/, '');
if (key && value) target[key] = value;
}
};
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
const end = (i + 1 < sections.length) ? sections[i + 1].start - 1 : lines.length;
const sectionLines = lines.slice(section.start, end);
for (const line of sectionLines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('//')) continue;
switch (section.type) {
case 'TERMS':
processLine(line, result.terms, result.multiPartTerms);
break;
case 'GENERAL_TERMS':
processLine(line, result.generalTerms, result.multiPartGeneralTerms);
break;
case 'FORBIDDEN_TERMS':
const term = trimmedLine.replace(/[,,]$/, '');
if (term) result.forbiddenTerms.push(term);
break;
case 'REGEX_TERMS':
const match = trimmedLine.match(/^(.+?)\s*[::]\s*(.*)$/s);
if (match) {
const pattern = match[1].trim();
const replacement = match[2].trim().replace(/[,,]$/, '');
if (pattern) {
result.regexTerms.push({ pattern, replacement });
}
}
break;
}
}
}
if (!result.metadata.version) {
throw new Error('文件格式错误:必须在文件头部包含 "版本号" 或 "version" 字段。');
}
if (Object.keys(result.terms).length === 0 && Object.keys(result.generalTerms).length === 0 &&
Object.keys(result.multiPartTerms).length === 0 && Object.keys(result.multiPartGeneralTerms).length === 0 &&
result.forbiddenTerms.length === 0 && result.regexTerms.length === 0) {
throw new Error('文件格式错误:必须包含至少一个有效词条区域 (词条, 通用词条, 禁翻词条, 正则表达式)。');
}
return result;
}
/**
* 从 GitHub 或 jsDelivr 导入在线术语表文件
*/
function importOnlineGlossary(url, options = {}) {
const { silent = false } = options;
return new Promise((resolve) => {
if (!url || !url.trim()) {
return resolve({ success: false, name: '未知', message: 'URL 不能为空。' });
}
const glossaryUrlRegex = /^(https:\/\/(raw\.githubusercontent\.com\/[^\/]+\/[^\/]+\/(?:refs\/heads\/)?[^\/]+|cdn\.jsdelivr\.net\/gh\/[^\/]+\/[^\/]+@[^\/]+)\/.+)$/;
if (!glossaryUrlRegex.test(url)) {
const message = "链接格式不正确。请输入一个有效的 GitHub Raw 或 jsDelivr 链接。";
if (!silent) alert(message);
return resolve({ success: false, name: url, message });
}
const filename = url.split('/').pop();
const lastDotIndex = filename.lastIndexOf('.');
const baseName = (lastDotIndex > 0) ? filename.substring(0, lastDotIndex) : filename;
const glossaryName = decodeURIComponent(baseName);
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function (response) {
if (response.status !== 200) {
const message = `下载 ${glossaryName} 术语表失败!服务器返回状态码: ${response.status}`;
if (!silent) notifyAndLog(message, '导入错误', 'error');
return resolve({ success: false, name: glossaryName, message });
}
try {
const onlineData = parseCustomGlossaryFormat(response.responseText);
const allImportedGlossaries = GM_getValue(IMPORTED_GLOSSARY_KEY, {});
allImportedGlossaries[url] = {
terms: onlineData.terms,
generalTerms: onlineData.generalTerms,
multiPartTerms: onlineData.multiPartTerms,
multiPartGeneralTerms: onlineData.multiPartGeneralTerms,
forbiddenTerms: onlineData.forbiddenTerms,
regexTerms: onlineData.regexTerms
};
GM_setValue(IMPORTED_GLOSSARY_KEY, allImportedGlossaries);
const metadata = GM_getValue(GLOSSARY_METADATA_KEY, {});
const existingMetadata = metadata[url] || {};
metadata[url] = { ...existingMetadata, ...onlineData.metadata, last_imported: getShanghaiTimeString() };
if (typeof metadata[url].enabled !== 'boolean') {
metadata[url].enabled = true;
}
GM_setValue(GLOSSARY_METADATA_KEY, metadata);
invalidateGlossaryCache();
const importedCount = Object.keys(onlineData.terms).length + Object.keys(onlineData.generalTerms).length +
Object.keys(onlineData.multiPartTerms).length + Object.keys(onlineData.multiPartGeneralTerms).length +
onlineData.regexTerms.length;
const message = `已成功导入 ${glossaryName} 术语表,共 ${importedCount} 个词条。版本号:v${onlineData.metadata.version || '未知'},维护者:${onlineData.metadata.maintainer || '未知'}。`;
if (!silent) {
notifyAndLog(message, '导入成功');
}
resolve({ success: true, name: glossaryName, message });
} catch (e) {
const message = `导入 ${glossaryName} 术语表失败:${e.message}`;
if (!silent) notifyAndLog(message, '处理错误', 'error');
resolve({ success: false, name: glossaryName, message });
}
},
onerror: function () {
const message = `下载 ${glossaryName} 术语表失败!请检查网络连接或链接。`;
if (!silent) notifyAndLog(message, '网络错误', 'error');
resolve({ success: false, name: glossaryName, message });
}
});
});
}
/**
* 比较版本号的函数
*/
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const len = Math.max(parts1.length, parts2.length);
for (let i = 0; i < len; i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
/**
* 检查术语表更新
*/
async function checkForGlossaryUpdates() {
const metadata = GM_getValue(GLOSSARY_METADATA_KEY, {});
const urls = Object.keys(metadata);
if (urls.length === 0) {
return;
}
for (const url of urls) {
try {
const response = await new Promise((resolve, reject) => {
const urlWithCacheBust = url + '?t=' + new Date().getTime();
GM_xmlhttpRequest({ method: 'GET', url: urlWithCacheBust, onload: resolve, onerror: reject, ontimeout: reject });
});
if (response.status !== 200) {
throw new Error(`服务器返回状态码: ${response.status}`);
}
const onlineData = parseCustomGlossaryFormat(response.responseText);
const currentMetadata = GM_getValue(GLOSSARY_METADATA_KEY, {});
const localVersion = currentMetadata[url]?.version;
const onlineVersion = onlineData.metadata.version;
const glossaryName = decodeURIComponent(url.split('/').pop().replace(/\.[^/.]+$/, ''));
if (!localVersion || compareVersions(onlineVersion, localVersion) > 0) {
const allImportedGlossaries = GM_getValue(IMPORTED_GLOSSARY_KEY, {});
allImportedGlossaries[url] = {
terms: onlineData.terms,
generalTerms: onlineData.generalTerms,
multiPartTerms: onlineData.multiPartTerms,
multiPartGeneralTerms: onlineData.multiPartGeneralTerms,
forbiddenTerms: onlineData.forbiddenTerms,
regexTerms: onlineData.regexTerms
};
currentMetadata[url] = { ...onlineData.metadata, last_updated: getShanghaiTimeString() };
GM_setValue(IMPORTED_GLOSSARY_KEY, allImportedGlossaries);
GM_setValue(GLOSSARY_METADATA_KEY, currentMetadata);
invalidateGlossaryCache();
Logger.info('数据', `术语表 ${glossaryName} 更新成功: v${localVersion} -> v${onlineVersion}`);
GM_notification(`检测到术语表 ${glossaryName} 新版本,已自动更新至 v${onlineVersion} 。`, 'AO3 Translator');
} else {
Logger.info('数据', `术语表 ${glossaryName} 已是最新版本 (v${localVersion})`);
}
} catch (e) {
Logger.warn('数据', `检查术语表更新失败 (${url})`, e.message);
}
}
}
/**
* 获取术语表规则,优先从缓存读取
*/
function getGlossaryRules() {
const cache = GM_getValue(GLOSSARY_RULES_CACHE_KEY, null);
const currentStateVersion = GM_getValue(GLOSSARY_STATE_VERSION_KEY, 0);
const currentScriptVersion = GM_info.script.version;
if (cache &&
cache.version === currentStateVersion &&
cache.scriptVersion === currentScriptVersion &&
cache.rules) {
Logger.info('数据', '命中术语表规则缓存');
return cache.rules.map(rule => {
if (rule.regex && typeof rule.regex === 'object' && rule.regex.source) {
try {
return { ...rule, regex: new RegExp(rule.regex.source, rule.regex.flags) };
} catch (e) {
return null;
}
}
return rule;
}).filter(Boolean);
}
Logger.info('数据', '缓存未命中、已失效或插件已更新,正在重建规则');
return buildPrioritizedGlossaryMaps();
}
/**
* 获取已预处理并编译完成的术语表规则
*/
async function getPreparedGlossaryRules() {
let currentStateVersion = GM_getValue(GLOSSARY_STATE_VERSION_KEY, 0);
if (runtimePreparedGlossaryCache && runtimePreparedGlossaryCache.version === currentStateVersion) {
Logger.info('数据', '命中术语表正则二级缓存');
return runtimePreparedGlossaryCache.preparedRules;
}
Logger.info('数据', '二级缓存未命中,正在构建术语匹配策略');
const rules = getGlossaryRules();
currentStateVersion = GM_getValue(GLOSSARY_STATE_VERSION_KEY, 0);
const domRules = rules.filter(r => r.matchStrategy === 'dom');
const regexStrategyRules = rules.filter(r => r.matchStrategy === 'regex');
const executionPlan = [];
let currentBatch = {
flags: null,
rules: []
};
const flushBatch = () => {
if (currentBatch.rules.length === 0) return;
const flags = currentBatch.flags;
const combinedPattern = currentBatch.rules.map(r => {
const source = r.regex.source;
return `(${source})`;
}).join('|');
try {
const combinedRegex = new RegExp(combinedPattern, flags);
executionPlan.push({
type: 'combined',
regex: combinedRegex,
rules: [...currentBatch.rules]
});
} catch (e) {
Logger.error('数据', `合并正则失败: ${e.message}`);
}
currentBatch.rules = [];
currentBatch.flags = null;
};
for (const rule of regexStrategyRules) {
if (rule.type === 'regex') {
flushBatch();
executionPlan.push({
type: 'single',
rule: rule
});
} else {
const flags = rule.regex.flags;
if (currentBatch.rules.length > 0 && currentBatch.flags !== flags) {
flushBatch();
}
currentBatch.flags = flags;
currentBatch.rules.push(rule);
}
}
flushBatch();
const preparedRules = {
domRules,
executionPlan
};
runtimePreparedGlossaryCache = {
version: currentStateVersion,
preparedRules
};
return preparedRules;
}
/**
* 安全解析术语表键值对
*/
function parseGlossaryKeyValuePair(entry) {
if (!entry) return null;
let inQuote = false;
let expectedCloseQuote = '';
let splitIndex = -1;
let separator = '';
const quotePairs = {
'"': '"',
"'": "'",
'“': '”',
'‘': '’'
};
const rawChars = entry.split('');
for (let i = 0; i < rawChars.length; i++) {
const char = rawChars[i];
if (inQuote) {
if (char === expectedCloseQuote) {
let nextNonSpaceChar = null;
for (let k = i + 1; k < rawChars.length; k++) {
if (!/\s/.test(rawChars[k])) {
nextNonSpaceChar = rawChars[k];
break;
}
}
const isSeparator =
nextNonSpaceChar === ':' ||
nextNonSpaceChar === ':' ||
nextNonSpaceChar === '=' ||
nextNonSpaceChar === '=';
if (isSeparator) {
inQuote = false;
}
}
} else {
if (quotePairs.hasOwnProperty(char)) {
inQuote = true;
expectedCloseQuote = quotePairs[char];
} else {
if (char === ':' || char === ':') {
splitIndex = i;
separator = ':';
break;
}
if (char === '=' || char === '=') {
splitIndex = i;
separator = '=';
break;
}
}
}
}
if (splitIndex === -1) return null;
const key = entry.substring(0, splitIndex).trim();
const value = entry.substring(splitIndex + 1).trim();
return { key, value, separator };
}
/**
* 构建并排序所有术语表规则
*/
function buildPrioritizedGlossaryMaps() {
const allImportedGlossaries = GM_getValue(IMPORTED_GLOSSARY_KEY, {});
const glossaryMetadata = GM_getValue(GLOSSARY_METADATA_KEY, {});
const localGlossaries = GM_getValue(CUSTOM_GLOSSARIES_KEY, []);
const onlineOrder = GM_getValue(ONLINE_GLOSSARY_ORDER_KEY, []);
const orderedGlossaries = [];
localGlossaries.forEach(g => {
if (g.enabled !== false) {
orderedGlossaries.push({ ...g, type: 'LOCAL', sourceName: g.name });
}
});
const onlineUrls = Object.keys(allImportedGlossaries);
const onlineUrlSet = new Set(onlineUrls);
onlineOrder.forEach(url => {
if (onlineUrlSet.has(url) && glossaryMetadata[url]?.enabled !== false) {
orderedGlossaries.push({ ...allImportedGlossaries[url], type: 'ONLINE', sourceName: decodeURIComponent(url.split('/').pop()) });
onlineUrlSet.delete(url);
}
});
onlineUrlSet.forEach(url => {
if (glossaryMetadata[url]?.enabled !== false) {
orderedGlossaries.push({ ...allImportedGlossaries[url], type: 'ONLINE', sourceName: decodeURIComponent(url.split('/').pop()) });
}
});
const validRules = [];
const processedInsensitiveTerms = new Set();
const processedSensitiveTerms = new Set();
const termSeparatorRegex = /[\s--﹣—–]+/;
const translationSeparatorRegex = /[\s·・]+/;
const quoteRegex = /["“‘'”’]/;
const smartSplit = (str, regex) => {
if (!quoteRegex.test(str)) return str.split(regex);
const parts = [];
let current = '';
let inQuote = false;
let currentQuote = '';
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (quoteRegex.test(char)) {
if (!inQuote) {
inQuote = true;
currentQuote = char;
} else if (char === currentQuote || (currentQuote === '“' && char === '”') || (currentQuote === '‘' && char === '’')) {
inQuote = false;
}
current += char;
} else if (!inQuote && regex.test(char)) {
if (current.trim()) parts.push(current.trim());
current = '';
} else {
current += char;
}
}
if (current.trim()) parts.push(current.trim());
return parts;
};
const sanitizeTranslation = (term, trans) => {
if (!trans || !quoteRegex.test(term)) return trans;
const match = trans.match(/^["“‘'](.*)["”’']$/);
if (match) return match[1].trim();
return trans;
};
const tryAddRule = (term, translation, glossaryIndex, sourceName, isSensitive, isForbidden, isRegex = false, isUnordered = false) => {
let normalizedTerm = term.trim();
if (!normalizedTerm) return;
let isLiteral = false;
const unquoted = smartUnquote(normalizedTerm);
if (unquoted !== normalizedTerm) {
isLiteral = true;
normalizedTerm = unquoted.trim();
}
const sanitizedTrans = sanitizeTranslation(normalizedTerm, translation);
const lowerTerm = normalizedTerm.toLowerCase();
if (processedInsensitiveTerms.has(lowerTerm)) {
return;
}
if (isSensitive) {
if (processedSensitiveTerms.has(normalizedTerm)) {
return;
}
processedSensitiveTerms.add(normalizedTerm);
} else {
processedInsensitiveTerms.add(lowerTerm);
}
let ruleObject;
const lengthBonus = normalizedTerm.length;
if (isRegex) {
try {
const testRegex = new RegExp(normalizedTerm);
if (testRegex.test("")) {
Logger.warn('数据', `术语表 ${sourceName} 中的正则 "${normalizedTerm}" 匹配空字符串,已跳过以防止死循环`);
return;
}
} catch (e) {
Logger.error('数据', `术语表 ${sourceName} 中的正则 "${normalizedTerm}" 非法: ${e.message}`);
return;
}
ruleObject = {
type: 'regex', matchStrategy: 'regex',
regex: new RegExp(normalizedTerm, 'g'),
replacement: translation,
glossaryIndex, source: sourceName, originalTerm: `${normalizedTerm}:${translation}`,
sortLength: lengthBonus, isSensitive
};
} else if (isLiteral) {
const escaped = normalizedTerm.replace(/([.*+?^${}()|[\]\\])/g, '\\$&');
const prefix = /^[a-zA-Z0-9]/.test(normalizedTerm) ? '\\b' : '';
const suffix = /[a-zA-Z0-9]$/.test(normalizedTerm) ? '\\b' : '';
const pattern = prefix + escaped + suffix;
const flags = isSensitive ? 'g' : 'gi';
ruleObject = {
type: isForbidden ? 'forbidden' : 'term', matchStrategy: 'regex',
regex: new RegExp(pattern, flags),
replacement: isForbidden ? normalizedTerm : sanitizedTrans,
glossaryIndex, source: sourceName, originalTerm: normalizedTerm,
sortLength: lengthBonus, isSensitive
};
} else {
const termParts = smartSplit(normalizedTerm, termSeparatorRegex);
const termForms = termParts.map(part => {
const partLiteralMatch = part.match(/^["“‘'](.*)["”’']$/);
if (partLiteralMatch) {
return new Set([partLiteralMatch[1].trim()]);
}
return Array.from(generateWordForms(part, { preserveCase: isForbidden, forceLowerCase: !isSensitive }));
});
ruleObject = {
type: isForbidden ? 'forbidden' : 'term', matchStrategy: 'dom',
parts: termForms,
replacement: isForbidden ? termForms.map(partForms => Array.from(partForms)[0]).join(' ') : sanitizedTrans,
glossaryIndex, isGeneral: !isSensitive, source: sourceName, originalTerm: normalizedTerm,
isUnordered: isUnordered,
sortLength: lengthBonus, isSensitive
};
}
validRules.push(ruleObject);
};
const processEqualsSyntax = (term, translation, glossaryIndex, sourceName, isSensitive) => {
tryAddRule(term, translation, glossaryIndex, sourceName, isSensitive, false, false, true);
if (term.match(/^["“‘'](.*)["”’']$/)) return;
const termParts = smartSplit(term, termSeparatorRegex);
const transParts = smartSplit(translation, translationSeparatorRegex);
if (termParts.length > 1 && termParts.length === transParts.length) {
for (let i = 0; i < termParts.length; i++) {
tryAddRule(termParts[i], transParts[i], glossaryIndex, sourceName, isSensitive, false, false, false);
}
}
};
const processStringRules = (rawString, glossaryIndex, sourceName, isSensitive, isForbidden) => {
if (!rawString) return;
const tokens = tokenizeQuoteAware(rawString, [',', ',']);
tokens.forEach(token => {
const entry = token.value.trim();
if (!entry) return;
if (isForbidden) {
tryAddRule(entry, null, glossaryIndex, sourceName, isSensitive, true);
return;
}
const parsed = parseGlossaryKeyValuePair(entry);
if (parsed) {
if (parsed.separator === '=') {
processEqualsSyntax(parsed.key, parsed.value, glossaryIndex, sourceName, isSensitive);
} else {
tryAddRule(parsed.key, parsed.value, glossaryIndex, sourceName, isSensitive, false);
}
}
});
};
orderedGlossaries.forEach((glossary, index) => {
const sourceName = glossary.sourceName;
if (glossary.forbidden) {
processStringRules(glossary.forbidden, index, sourceName, true, true);
}
(glossary.forbiddenTerms || []).forEach(term => {
tryAddRule(term, null, index, sourceName, true, true);
});
if (glossary.sensitive) {
processStringRules(glossary.sensitive, index, sourceName, true, false);
}
Object.entries(glossary.terms || {}).forEach(([k, v]) => tryAddRule(k, v, index, sourceName, true, false));
Object.entries(glossary.multiPartTerms || {}).forEach(([k, v]) => processEqualsSyntax(k, v, index, sourceName, true));
if (glossary.insensitive) {
processStringRules(glossary.insensitive, index, sourceName, false, false);
}
Object.entries(glossary.generalTerms || {}).forEach(([k, v]) => tryAddRule(k, v, index, sourceName, false, false));
Object.entries(glossary.multiPartGeneralTerms || {}).forEach(([k, v]) => processEqualsSyntax(k, v, index, sourceName, false));
(glossary.regexTerms || []).forEach(({ pattern, replacement }) => {
tryAddRule(pattern, replacement, index, sourceName, true, false, true);
});
});
validRules.sort((a, b) => {
if (a.glossaryIndex !== b.glossaryIndex) {
return a.glossaryIndex - b.glossaryIndex;
}
const typeScore = { 'forbidden': 100, 'term': 50, 'regex': 50 };
const scoreA = typeScore[a.type] || 0;
const scoreB = typeScore[b.type] || 0;
if (scoreB !== scoreA) {
return scoreB - scoreA;
}
if (b.sortLength !== a.sortLength) {
return b.sortLength - a.sortLength;
}
return (b.isSensitive ? 1 : 0) - (a.isSensitive ? 1 : 0);
});
const currentStateVersion = generateGlossaryStateVersion();
const serializedRules = validRules.map(rule => {
if (rule.regex instanceof RegExp) {
return { ...rule, regex: { source: rule.regex.source, flags: rule.regex.flags } };
}
return rule;
});
GM_setValue(GLOSSARY_RULES_CACHE_KEY, {
version: currentStateVersion,
scriptVersion: GM_info.script.version,
rules: serializedRules
});
Logger.info('数据', `术语表规则重建完成,当前版本: v${currentStateVersion}`);
return validRules;
}
/**
* 为单个英文单词生成其常见词形变体
*/
function generateWordForms(baseTerm, options = {}) {
const { preserveCase = false, forceLowerCase = false } = options;
const forms = new Set();
if (!baseTerm || typeof baseTerm !== 'string') {
return forms;
}
forms.add(baseTerm);
if (!/[a-zA-Z]$/.test(baseTerm)) {
if (forceLowerCase) {
forms.add(baseTerm.toLowerCase());
}
return forms;
}
const lowerBase = baseTerm.toLowerCase();
let pluralEnding;
let baseWithoutEnding = baseTerm;
if (lowerBase.endsWith('y') && !['a', 'e', 'i', 'o', 'u'].includes(lowerBase.slice(-2, -1))) {
pluralEnding = 'ies';
baseWithoutEnding = baseTerm.slice(0, -1);
} else if (/[sxz]$/i.test(lowerBase) || /(ch|sh)$/i.test(lowerBase)) {
pluralEnding = 'es';
} else {
pluralEnding = 's';
}
let pluralForm;
if (preserveCase) {
if (baseTerm === lowerBase) {
pluralForm = baseWithoutEnding + pluralEnding;
} else if (baseTerm === baseTerm.toUpperCase()) {
pluralForm = (baseWithoutEnding + pluralEnding).toUpperCase();
} else if (baseTerm.length > 0 && baseTerm[0] === baseTerm[0].toUpperCase() && baseTerm.slice(1) === baseTerm.slice(1).toLowerCase()) {
const pluralBase = baseWithoutEnding + pluralEnding;
pluralForm = pluralBase.charAt(0).toUpperCase() + pluralBase.slice(1).toLowerCase();
} else {
pluralForm = baseWithoutEnding + pluralEnding.toLowerCase();
}
} else {
pluralForm = baseWithoutEnding + pluralEnding;
}
forms.add(pluralForm);
if (forceLowerCase) {
const lowerCaseForms = new Set();
forms.forEach(form => lowerCaseForms.add(form.toLowerCase()));
return lowerCaseForms;
}
return forms;
}
/**
* 解析“译文后处理替换”规则字符串为对象
*/
function parsePostReplaceString(rawInput) {
const rules = {
singleRules: {},
multiPartRules: []
};
if (typeof rawInput !== 'string' || !rawInput.trim()) {
return rules;
}
const internalSeparatorRegex = /[\s--﹣—–]+/;
const internalSeparatorGlobalRegex = /[\s--﹣—–]+/g;
rawInput.split(/[,,]/).forEach(entry => {
const trimmedEntry = entry.trim();
if (!trimmedEntry) return;
const multiPartMatch = trimmedEntry.match(/^(.*?)\s*[==]\s*(.*?)$/);
if (multiPartMatch) {
const source = multiPartMatch[1].trim();
const target = multiPartMatch[2].trim();
if (source && target) {
const sourceParts = source.split(internalSeparatorRegex);
const targetParts = target.split(internalSeparatorRegex);
const multiPartRule = {
source: source.replace(internalSeparatorGlobalRegex, ' '),
target: target.replace(internalSeparatorGlobalRegex, ' '),
subRules: {}
};
if (sourceParts.length === targetParts.length && sourceParts.length > 1) {
for (let i = 0; i < sourceParts.length; i++) {
multiPartRule.subRules[sourceParts[i]] = targetParts[i];
}
}
rules.multiPartRules.push(multiPartRule);
}
} else {
const singlePartMatch = trimmedEntry.match(/^(.*?)\s*[::]\s*(.+?)\s*$/);
if (singlePartMatch) {
const key = singlePartMatch[1].trim();
const value = singlePartMatch[2].trim();
if (key) {
rules.singleRules[key] = value;
}
}
}
});
return rules;
}
/**
* 译文后处理替换
*/
function applyPostTranslationReplacements(text) {
const rulesList = GM_getValue(POST_REPLACE_RULES_KEY, []);
if (!rulesList || rulesList.length === 0) {
return text;
}
let processedText = text;
for (const ruleConfig of rulesList) {
if (!ruleConfig.enabled) continue;
const rulesData = parsePostReplaceString(ruleConfig.content);
const { singleRules = {}, multiPartRules = [] } = rulesData;
const finalReplacementMap = {};
multiPartRules.forEach(rule => {
Object.assign(finalReplacementMap, rule.subRules);
});
Object.assign(finalReplacementMap, singleRules);
multiPartRules.forEach(rule => {
finalReplacementMap[rule.source] = rule.target;
});
const keys = Object.keys(finalReplacementMap);
if (keys.length === 0) {
continue;
}
const sortedKeys = keys.sort((a, b) => b.length - a.length);
const regex = new RegExp(sortedKeys.map(key => key.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|'), 'g');
processedText = processedText.replace(regex, (matched) => finalReplacementMap[matched]);
}
return processedText;
}
/**
* 通用通知与日志函数
*/
function notifyAndLog(message, title = 'AO3 Translator', logType = 'info') {
GM_notification(message, title);
if (logType === 'error') {
Logger.error('系统', message);
} else {
Logger.info('系统', message);
}
}
/**
* sleepms 函数:延时。
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 获取当前时间的上海时区格式化字符串
*/
function getShanghaiTimeString() {
const now = new Date();
const year = now.toLocaleString('en-US', { year: 'numeric', timeZone: 'Asia/Shanghai' });
const month = now.toLocaleString('en-US', { month: '2-digit', timeZone: 'Asia/Shanghai' });
const day = now.toLocaleString('en-US', { day: '2-digit', timeZone: 'Asia/Shanghai' });
const time = now.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'Asia/Shanghai'
});
return `${year}-${month}-${day} ${time}`;
}
/**
* 根据术语表内容生成一个状态哈希
*/
function generateGlossaryStateVersion() {
const current = GM_getValue(GLOSSARY_STATE_VERSION_KEY, 0);
const next = current + 1;
GM_setValue(GLOSSARY_STATE_VERSION_KEY, next);
return next;
}
/**
* 使术语表规则缓存失效
*/
function invalidateGlossaryCache() {
GM_deleteValue(GLOSSARY_RULES_CACHE_KEY);
generateGlossaryStateVersion();
runtimePreparedGlossaryCache = null;
Logger.info('数据', '术语表规则缓存已失效');
}
/**
* getNestedProperty 函数:获取嵌套属性的安全函数。
* @param {Object} obj - 需要查询的对象
* @param {string} path - 属性路径
* @returns {*} - 返回嵌套属性的值
*/
function getNestedProperty(obj, path) {
return path.split('.').reduce((acc, part) => {
const match = part.match(/(\w+)(?:\[(\d+)\])?/);
if (!match) return undefined;
const key = match[1];
const index = match[2];
if (acc && typeof acc === 'object' && acc[key] !== undefined) {
return index !== undefined ? acc[key][index] : acc[key];
}
return undefined;
}, obj);
}
/**
* 翻译文本处理函数
*/
const AdvancedTranslationCleaner = new (class {
constructor() {
this.metaKeywords = [
'原文', '输出', '说明', '润色', '语境', '遵守', '指令',
'Original text', 'Output', 'Note', 'Stage', 'Strategy', 'Polish', 'Retain', 'Glossary', 'Adherence'
];
this.junkLineRegex = new RegExp(`^\\s*(\\d+\\.\\s*)?(${this.metaKeywords.join('|')})[::\\s]`, 'i');
this.lineNumbersRegex = /^\d+\.\s*/;
this.aiGenericExplanationRegex = /\s*[\uff08(](?:原文|译文|说明|保留|注释|译注|注)[::\s][^\uff08\uff09()]*?[\uff09)]\s*/g;
this.cjkIdeographs = '\\u4e00-\\u9fff\\u3400-\\u4dbf\\u2e80-\\u2eff\\uf900-\\ufaff';
this.cjkSymbols = '\\u3000-\\u303f\\uff00-\\uffef\\u30fb';
this.cjkTypoQuotes = '\\u2018-\\u201d\\u2026';
this.cjkBoundaryChars = this.cjkIdeographs + this.cjkTypoQuotes;
this.cjkAll = this.cjkBoundaryChars + this.cjkSymbols;
}
clean(text) {
if (!text || typeof text !== 'string') {
return '';
}
let cleanedText = text
.replace(/ /g, ' ')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/\u00a0/g, ' ');
cleanedText = cleanedText.split('\n').filter(line => !this.junkLineRegex.test(line)).join('\n');
cleanedText = cleanedText.replace(this.lineNumbersRegex, '');
cleanedText = cleanedText.replace(this.aiGenericExplanationRegex, '');
cleanedText = cleanedText.replace(/(<(em|strong|span|b|i|u)[^>]*>)([\s\S]*?)(<\/\2>)/g, (_match, openTag, _tagName, content, closeTag) => {
return openTag + content.trim() + closeTag;
});
const cjkBoundaryBlock = `[${this.cjkBoundaryChars}]`;
const latinChar = `[a-zA-Z0-9_.-]`;
const simpleFormattingTags = `?(?:em|strong|span|b|i|u)>`;
const cjkContext = `(?:[${this.cjkAll}]|${simpleFormattingTags})`;
cleanedText = cleanedText.replace(new RegExp(`(${cjkBoundaryBlock})((?:${simpleFormattingTags})*)(${latinChar}+)`, 'g'), '$1$2 $3');
cleanedText = cleanedText.replace(new RegExp(`(${latinChar}+)((?:${simpleFormattingTags})*)(${cjkBoundaryBlock})`, 'g'), '$1 $2$3');
cleanedText = cleanedText.replace(/(“|‘|「|『)\s+/g, '$1');
cleanedText = cleanedText.replace(/\s+(”|’|」|』)/g, '$1');
cleanedText = cleanedText.replace(/\s+/g, ' ');
const cjkSpaceRegex = new RegExp(`(${cjkContext})\\s+(?=${cjkContext})`, 'g');
let prevText;
do {
prevText = cleanedText;
cleanedText = cleanedText.replace(cjkSpaceRegex, '$1');
} while (cleanedText !== prevText);
return cleanedText.trim();
}
})();
/**
* 通用后处理函数:处理块级元素末尾的孤立标点
*/
function handleTrailingPunctuation(rootElement = document) {
const selectors = 'p, li, dd, blockquote, h1, h2, h3, h4, h5, h6, .summary, .notes';
const punctuationMap = { '.': ' 。', '?': ' ?', '!': ' !' };
const elements = rootElement.querySelectorAll(`${selectors}:not([data-translated-by-custom-function])`);
elements.forEach(el => {
let lastMeaningfulNode = el.lastChild;
while (lastMeaningfulNode) {
if (lastMeaningfulNode.nodeType === Node.COMMENT_NODE ||
(lastMeaningfulNode.nodeType === Node.TEXT_NODE && lastMeaningfulNode.nodeValue.trim() === '')) {
lastMeaningfulNode = lastMeaningfulNode.previousSibling;
} else {
break;
}
}
if (
lastMeaningfulNode &&
lastMeaningfulNode.nodeType === Node.TEXT_NODE
) {
const trimmedText = lastMeaningfulNode.nodeValue.trim();
if (punctuationMap[trimmedText]) {
lastMeaningfulNode.nodeValue = lastMeaningfulNode.nodeValue.replace(trimmedText, punctuationMap[trimmedText]);
el.setAttribute('data-translated-by-custom-function', 'true');
}
}
});
}
/**
* 通用函数:对页面上所有“分类”复选框区域进行重新排序。
*/
function reorderCategoryCheckboxes() {
const containers = document.querySelectorAll('div[id$="_category_tagnames_checkboxes"]');
containers.forEach(container => {
if (container.dataset.reordered === 'true') {
return;
}
const list = container.querySelector('ul.options');
if (!list) return;
const desiredOrder = ['F/F', 'F/M', 'Gen', 'M/M', 'Multi', 'Other'];
const itemsMap = new Map();
list.querySelectorAll('li').forEach(item => {
const checkbox = item.querySelector('input[type="checkbox"]');
if (checkbox) {
itemsMap.set(checkbox.value, item);
}
});
desiredOrder.forEach(value => {
const itemToMove = itemsMap.get(value);
if (itemToMove) {
list.appendChild(itemToMove);
}
});
container.dataset.reordered = 'true';
});
}
/**
* 通用函数:重新格式化包含标准日期组件的元素。
* @param {Element} containerElement - 直接包含日期组件的元素
*/
function reformatDateInElement(containerElement) {
if (!containerElement || containerElement.hasAttribute('data-reformatted')) {
return;
}
const dayEl = containerElement.querySelector('abbr.day');
const dateEl = containerElement.querySelector('span.date');
const monthEl = containerElement.querySelector('abbr.month');
const yearEl = containerElement.querySelector('span.year');
if (!dayEl || !dateEl || !monthEl || !yearEl) {
return;
}
// 翻译星期
let dayFull = dayEl.getAttribute('title');
dayFull = fetchTranslatedText(dayFull) || dayFull;
// 翻译月份
const monthText = monthEl.textContent;
const translatedMonth = fetchTranslatedText(monthText) || monthText;
// 格式化时间
const timeEl = containerElement.querySelector('span.time');
let formattedTime = '';
if (timeEl) {
const timeText = timeEl.textContent;
const T = timeText.slice(0, -2);
const ampm = timeText.slice(-2);
if (ampm === 'PM') {
formattedTime = '下午 ' + T;
} else if (ampm === 'AM') {
formattedTime = (T.startsWith('12') ? '凌晨 ' : '上午 ') + T;
} else {
formattedTime = timeText;
}
}
// 提取时区
const timezoneEl = containerElement.querySelector('abbr.timezone');
const timezoneText = timezoneEl ? timezoneEl.textContent : 'UTC';
// 替换内容
const prefixNode = containerElement.firstChild;
let prefixText = '';
if (prefixNode && prefixNode.nodeType === Node.TEXT_NODE) {
prefixText = prefixNode.nodeValue;
}
containerElement.innerHTML = '';
if (prefixText) {
containerElement.appendChild(document.createTextNode(prefixText));
}
containerElement.appendChild(document.createTextNode(`${yearEl.textContent}年${translatedMonth}${dateEl.textContent}日 ${dayFull} ${formattedTime} ${timezoneText}`));
containerElement.setAttribute('data-reformatted', 'true');
}
/**
* 执行数据迁移,将旧版存储格式更新为新版
*/
function runDataMigration() {
const CURRENT_MIGRATION_VERSION = 1;
const savedVersion = GM_getValue('ao3_migration_version', 0);
if (savedVersion >= CURRENT_MIGRATION_VERSION) {
return;
}
{
const newRules = GM_getValue(POST_REPLACE_RULES_KEY, null);
if (newRules === null) {
const oldString = GM_getValue(POST_REPLACE_STRING_KEY, '');
if (oldString) {
const defaultRule = {
id: `replace_${Date.now()}`,
name: '默认',
content: oldString,
enabled: true
};
GM_setValue(POST_REPLACE_RULES_KEY, [defaultRule]);
GM_setValue(POST_REPLACE_SELECTED_ID_KEY, defaultRule.id);
GM_setValue(POST_REPLACE_EDIT_MODE_KEY, 'settings');
} else {
const postReplaceData = GM_getValue(POST_REPLACE_MAP_KEY, null);
if (postReplaceData && typeof postReplaceData === 'object' && !postReplaceData.hasOwnProperty('singleRules')) {
GM_setValue(POST_REPLACE_MAP_KEY, {
singleRules: postReplaceData,
multiPartRules: []
});
}
}
}
}
{
const newGlossaries = GM_getValue(CUSTOM_GLOSSARIES_KEY, null);
if (newGlossaries !== null) {
let changed = false;
newGlossaries.forEach(g => {
if (typeof g.enabled === 'undefined') {
g.enabled = true;
changed = true;
}
});
if (changed) {
GM_setValue(CUSTOM_GLOSSARIES_KEY, newGlossaries);
}
} else {
const oldGlossaryStr = GM_getValue('ao3_local_glossary_string', '');
const oldForbiddenStr = GM_getValue('ao3_local_forbidden_string', '');
let finalSensitive = oldGlossaryStr;
if (!finalSensitive) {
const oldGlossaryObj = GM_getValue('ao3_local_glossary') || GM_getValue('ao3_translation_glossary');
if (oldGlossaryObj && typeof oldGlossaryObj === 'object') {
finalSensitive = Object.entries(oldGlossaryObj).map(([k, v]) => `${k}:${v}`).join(', ');
}
}
let finalForbidden = oldForbiddenStr;
if (!finalForbidden) {
const oldForbiddenArray = GM_getValue('ao3_local_forbidden_terms');
if (Array.isArray(oldForbiddenArray)) {
finalForbidden = oldForbiddenArray.join(', ');
}
}
if (finalSensitive || finalForbidden) {
const defaultGlossary = {
id: `local_${Date.now()}`,
name: '默认',
sensitive: finalSensitive || '',
insensitive: '',
forbidden: finalForbidden || '',
enabled: true
};
GM_setValue(CUSTOM_GLOSSARIES_KEY, [defaultGlossary]);
}
['ao3_local_glossary_string', 'ao3_local_forbidden_string', 'ao3_local_glossary',
'ao3_translation_glossary', 'ao3_local_forbidden_terms'].forEach(key => GM_deleteValue(key));
}
}
{
const servicesToMigrate = ['zhipu_ai', 'deepseek_ai', 'groq_ai', 'together_ai', 'cerebras_ai', 'modelscope_ai'];
servicesToMigrate.forEach(serviceName => {
const oldKeyName = `${serviceName.split('_')[0]}_api_key`;
const newStringKey = `${serviceName}_keys_string`;
const newArrayKey = `${serviceName}_keys_array`;
const oldKeyValue = GM_getValue(oldKeyName);
if (oldKeyValue && GM_getValue(newStringKey) === undefined) {
GM_setValue(newStringKey, oldKeyValue);
const keysArray = oldKeyValue.replace(/[,]/g, ',').split(',').map(k => k.trim()).filter(Boolean);
GM_setValue(newArrayKey, keysArray);
GM_deleteValue(oldKeyName);
}
});
const oldChatglmKey = GM_getValue('chatglm_api_key');
if (oldChatglmKey && GM_getValue('zhipu_ai_keys_string') === undefined) {
GM_setValue('zhipu_ai_keys_string', oldChatglmKey);
GM_setValue('zhipu_ai_keys_array', [oldChatglmKey]);
GM_deleteValue('chatglm_api_key');
}
}
{
const oldKeysArray = GM_getValue('google_ai_keys_array');
const newKeysString = GM_getValue('google_ai_keys_string');
if (Array.isArray(oldKeysArray) && !newKeysString) {
GM_setValue('google_ai_keys_string', oldKeysArray.join(', '));
}
}
{
const modelKey = 'google_ai_model';
const currentModel = GM_getValue(modelKey);
const migrationMap = {
'gemini-2.5-flash': 'gemini-flash-latest',
'gemini-2.5-flash-lite': 'gemini-flash-lite-latest'
};
if (currentModel && migrationMap[currentModel]) {
GM_setValue(modelKey, migrationMap[currentModel]);
}
}
{
const sysPrompt = GM_getValue('custom_ai_system_prompt');
if (typeof sysPrompt === 'string' && sysPrompt.includes('${')) {
GM_setValue('custom_ai_system_prompt', sysPrompt.replace(/\$\{/g, '{'));
}
}
{
const targetThresholds = '10, 0.8, 10, 5';
const globalKey = 'custom_ai_validation_thresholds';
GM_setValue(globalKey, targetThresholds);
const profiles = GM_getValue(AI_PROFILES_KEY);
if (Array.isArray(profiles)) {
let changed = false;
profiles.forEach(p => {
if (p.params) {
p.params.validation_thresholds = targetThresholds;
changed = true;
}
});
if (changed) {
GM_setValue(AI_PROFILES_KEY, profiles);
}
}
}
const keysToCheck = [
{ key: 'enable_RegExp', default: DEFAULT_CONFIG.GENERAL.enable_RegExp },
{ key: 'enable_transDesc', default: DEFAULT_CONFIG.GENERAL.enable_transDesc },
{ key: 'enable_ui_trans', default: DEFAULT_CONFIG.GENERAL.enable_ui_trans },
{ key: 'show_fab', default: DEFAULT_CONFIG.GENERAL.show_fab },
{ key: 'enable_debug_mode', default: DEFAULT_CONFIG.GENERAL.enable_debug_mode },
{ key: 'translation_display_mode', default: DEFAULT_CONFIG.GENERAL.translation_display_mode },
{ key: 'from_lang', default: DEFAULT_CONFIG.GENERAL.from_lang },
{ key: 'to_lang', default: DEFAULT_CONFIG.GENERAL.to_lang },
{ key: 'lang_detector', default: DEFAULT_CONFIG.GENERAL.lang_detector },
{ key: 'transEngine', default: DEFAULT_CONFIG.ENGINE.current },
{ key: 'custom_url_first_save_done', default: DEFAULT_CONFIG.GENERAL.custom_url_first_save_done }
];
let cleanedCount = 0;
keysToCheck.forEach(item => {
const currentValue = GM_getValue(item.key);
if (currentValue === item.default) {
GM_deleteValue(item.key);
cleanedCount++;
}
});
Logger.info('系统', `配置清理完成,移除了 ${cleanedCount} 个未修改的默认设置`);
GM_setValue('ao3_migration_version', CURRENT_MIGRATION_VERSION);
}
/**
* 脚本主入口
*/
function main() {
if (window.top !== window.self) {
return;
}
if (window.ao3_translator_running) {
Logger.warn('系统', '检测到脚本重复执行,已拦截');
return;
}
window.ao3_translator_running = true;
Logger.info('系统', `插件初始化完成,当前版本:v${GM_info.script.version}`);
ProfileManager.init();
FormattingManager.init();
runDataMigration();
updateBlockerCache();
checkForGlossaryUpdates();
const fabElements = createFabUI();
const panelElements = createSettingsPanelUI();
let rerenderMenu;
let fabLogic;
const handlePanelClose = () => {
if (fabLogic) {
fabLogic.retractFab();
}
};
const panelLogic = initializeSettingsPanelLogic(panelElements, () => rerenderMenu(), handlePanelClose);
fabLogic = initializeFabInteraction(fabElements, panelLogic);
document.addEventListener('click', (e) => {
if (!e.altKey || e.button !== 0) return;
const link = e.target.closest('a');
if (!link) return;
let added = false;
const href = link.getAttribute('href');
const text = link.textContent.trim();
if (href && /\/works\/\d+$/.test(href)) {
const idMatch = href.match(/\/works\/(\d+)$/);
if (idMatch) {
addBlockRule('ao3_blocker_content_id', idMatch[1]);
added = true;
}
}
else if (link.rel && link.rel.includes('author')) {
addBlockRule('ao3_blocker_content_author', text);
added = true;
}
else if (link.classList.contains('tag')) {
addBlockRule('ao3_blocker_tags_black', `'${text}'`);
added = true;
}
if (added) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
link.style.pointerEvents = 'none';
setTimeout(() => { link.style.pointerEvents = ''; }, 100);
refreshBlocker('incremental');
}
}, true);
const globalStyles = document.createElement('style');
globalStyles.textContent = `
.autocomplete.dropdown p.notice {
margin-bottom: 0;
}
.translated-by-ao3-translator, .translated-by-ao3-translator-error {
margin-top: 15px;
margin-bottom: 15px;
}
li.post .userstuff {
margin-bottom: 15px;
}
li.post .userstuff .translated-by-ao3-translator {
margin-bottom: 0;
}
.translate-me-ao3-wrapper {
border: none;
background: transparent;
box-shadow: none;
margin-top: 15px;
margin-bottom: 5px;
clear: both;
display: block;
}
.translate-me-ao3-button {
color: #1b95e0;
font-size: small;
cursor: pointer;
display: inline-block;
margin-left: 10px;
}
.translated-tags-container {
margin-top: 15px;
margin-bottom: 10px;
}
.collection.profile .primary.header.module .translate-me-ao3-wrapper,
.collection.home .primary.header.module .translate-me-ao3-wrapper {
clear: none !important;
margin-left: 120px !important;
margin-top: 15px !important;
width: auto !important;
}
p.kudos {
line-height: 1.5;
}
.retry-translation-button {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
margin-left: 8px;
cursor: pointer;
color: #1b95e0;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.retry-translation-button:hover {
color: #0d8bd9;
}
.retry-translation-button:active {
opacity: 0.7;
}
.retry-translation-button svg {
width: 18px;
height: 18px;
fill: currentColor;
}
/* 标签翻译样式 */
.ao3-tag-translation {
margin-left: 6px;
opacity: 0.85;
font-size: 0.95em;
display: inline;
color: inherit;
}
/* 当处于仅译文模式时,移除左侧间隔 */
body.ao3-translation-only .ao3-tag-translation {
margin-left: 0;
}
/* 标签原文包裹样式 */
.ao3-tag-original {
display: inline;
}
/* 防止在标签列表中换行导致布局错乱 */
li.blurb ul.tags li,
dl.meta dd ul.tags li,
ul.tags.commas li {
display: inline;
}
`;
document.head.appendChild(globalStyles);
if (document.documentElement.lang !== CONFIG.LANG) {
document.documentElement.lang = CONFIG.LANG;
}
new MutationObserver(() => {
if (document.documentElement.lang !== CONFIG.LANG && document.documentElement.lang.toLowerCase().startsWith('en')) {
document.documentElement.lang = CONFIG.LANG;
}
}).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
updatePageConfig('初始载入');
if (pageConfig.currentPageType) {
if (FeatureSet.enable_ui_trans) {
transTitle();
transBySelector();
traverseNode(document.body);
runHighPriorityFunctions();
}
scanAllWorks();
fabLogic.toggleFabVisibility();
if (FeatureSet.enable_transDesc) {
setTimeout(transDesc, 1000);
}
}
rerenderMenu = setupMenuCommands(fabLogic, panelLogic);
rerenderMenu();
watchUpdate(fabLogic);
}
/**
* 监视页面变化
*/
function watchUpdate(fabLogic) {
let previousURL = window.location.href;
const handleUrlChange = () => {
const currentURL = window.location.href;
if (currentURL !== previousURL) {
previousURL = currentURL;
updatePageConfig('URL变化');
if (FeatureSet.enable_ui_trans) {
transTitle();
transBySelector();
traverseNode(document.body);
runHighPriorityFunctions();
}
scanAllWorks();
fabLogic.toggleFabVisibility();
if (FeatureSet.enable_transDesc) {
transDesc();
}
}
};
const processMutations = mutations => {
if (BlockerCache.enabled) {
let hasNewBlurbs = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
if (node.classList.contains('blurb') || node.querySelector('li.blurb')) {
hasNewBlurbs = true;
break;
}
}
}
}
if (hasNewBlurbs) break;
}
if (hasNewBlurbs) {
checkWorksSynchronously();
}
}
const nodesToProcess = mutations.flatMap(({ target, addedNodes, type }) => {
if (type === 'childList' && addedNodes.length > 0) {
return Array.from(addedNodes);
}
if (type === 'attributes' || (type === 'characterData' && pageConfig.characterData)) {
return [target];
}
return [];
});
if (nodesToProcess.length === 0) return;
const uniqueNodes = [...new Set(nodesToProcess)];
uniqueNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE || node.parentElement) {
if (FeatureSet.enable_ui_trans) {
traverseNode(node);
runHighPriorityFunctions(node.parentElement || node);
}
}
});
fabLogic.toggleFabVisibility();
if (FeatureSet.enable_transDesc) {
transDesc();
}
};
const observer = new MutationObserver(mutations => {
handleUrlChange();
if (window.location.href === previousURL) {
processMutations(mutations);
}
});
observer.observe(document.documentElement, { ...CONFIG.OBSERVER_CONFIG, subtree: true });
}
/**
* 辅助函数:集中调用所有高优先级专用函数
* @param {HTMLElement} [rootElement=document] - 扫描范围
*/
function runHighPriorityFunctions(rootElement = document) {
if (!rootElement || typeof rootElement.querySelectorAll !== 'function') {
return;
}
const innerHTMLRules = pageConfig.innerHTMLRules || [];
if (innerHTMLRules.length > 0) {
innerHTMLRules.forEach(rule => {
if (!Array.isArray(rule) || rule.length !== 3) return;
const [selector, regex, replacement] = rule;
try {
rootElement.querySelectorAll(selector).forEach(el => {
if (el.hasAttribute('data-translated-by-custom-function')) return;
if (pageConfig.ignoreSelectors && el.closest(pageConfig.ignoreSelectors)) return;
if (regex.test(el.innerHTML)) {
el.innerHTML = el.innerHTML.replace(regex, replacement);
el.setAttribute('data-translated-by-custom-function', 'true');
}
});
} catch (e) { /* 忽略无效的选择器 */ }
});
}
const kudosDiv = rootElement.querySelector('#kudos');
if (kudosDiv && !kudosDiv.dataset.kudosObserverAttached) {
translateKudosSection();
}
// 通用的后处理和格式化函数
handleTrailingPunctuation(rootElement);
translateSymbolsKeyModal(rootElement);
translateFirstLoginBanner();
translateBookmarkSymbolsKeyModal();
translateRatingHelpModal();
translateCategoriesHelp();
translateRelationshipsHelp();
translateCharactersHelp();
translateAdditionalTagsHelp();
translateCollectionsHelp();
translateRecipientsHelp();
translateParentWorksHelp();
translateChoosingSeriesHelp();
translateBackdatingHelp();
translateLanguagesHelp();
translateWorkSkins();
translateRegisteredUsers();
translateCommentsModerated();
translateFandomHelpModal();
translateWhoCanComment();
translateWorkImportTroubleshooting();
translateEncodingHelp();
translatePrivacyPreferences();
translateDisplayPreferences();
translateSkinsBasics();
translateWorkTitleFormat();
translateCommentPreferences();
translateCollectionPreferences();
translateMiscPreferences();
translateTagFiltersIncludeTags();
translateTagFiltersExcludeTags();
translateBookmarkFiltersIncludeTags();
translateWorkSearchTips();
translateChapterTitleHelpModal();
translateActionButtons();
translateSortButtons();
translateBookmarkFiltersExcludeTags();
translateSearchResultsHeader();
translateWorkSearchResultsHelp();
translateSkinsApprovalModal();
translateSkinsCreatingModal();
translateSkinsConditionsModal();
translateSkinsParentsModal();
translateSkinsWizardFontModal();
translateSkinsWizardFontSizeModal();
translateSkinsWizardVerticalGapModal();
translateSkinsWizardAccentColorModal();
translateCollectionNameHelpModal();
translateIconAltTextHelpModal();
translatePseudIconCommentHelpModal();
translateCollectionModeratedHelpModal();
translateCollectionClosedHelpModal();
translateTagSearchResultsHelp();
translateChallengeAnyTips();
translateOptionalTagsHelp();
translateBookmarkSearchTips();
translateWarningHelpModal();
translateHtmlHelpModal();
translateRteHelpModal();
translateBookmarkSearchResultsHelpModal();
translateTagsetAboutModal();
translateFlashMessages();
translateTagSetsHeading();
translateFoundResultsHeading();
translateTOSPrompt();
translateHeadingTags();
// 统一寻找并重新格式化所有日期容器
const dateSelectors = [
'.header.module .meta span.published',
'li.collection .summary p:has(abbr.day)',
'.comment .posted.datetime',
'.comment .edited.datetime',
'dd.datetime',
'p:has(> span.datetime)',
'p.caution.notice > span:has(abbr.day)',
'p.notice > span:has(abbr.day)',
'div.flash.notice span.datetime',
];
rootElement.querySelectorAll(dateSelectors.join(', '))
.forEach(reformatDateInElement);
// 根据当前页面类型,调用页面专属的翻译和处理函数
const pageType = pageConfig.currentPageType;
if (pageType === 'about_page') {
translateAboutPage();
}
if (pageType === 'diversity_statement') {
translateDiversityStatement();
}
if (pageType === 'donate_page') {
translateDonatePage();
}
if (pageType === 'tag_sets_new' || pageType === 'collections_dashboard_common') {
reorderCategoryCheckboxes();
}
if (pageType === 'front_page') {
translateFrontPageIntro();
}
if (pageType === 'invite_requests_index') {
translateInvitationRequestsPage();
}
if (pageType === 'error_too_many_requests') {
translateTooManyRequestsPage();
}
if (pageType === 'works_search') {
translateWorkSearchDateTips();
translateWorkSearchCrossoverTips();
translateWorkSearchNumericalTips();
translateWorkSearchLanguageTips();
translateWorkSearchTagsTips();
}
if (pageType === 'people_search') {
translatePeopleSearchTips();
}
if (pageType === 'bookmarks_search') {
translateBookmarkSearchWorkTagsTips();
translateBookmarkSearchTypeTips();
translateBookmarkSearchDateUpdatedTips();
translateBookmarkSearchBookmarkerTagsTips();
translateBookmarkSearchRecTips();
translateBookmarkSearchNotesTips();
translateBookmarkSearchDateBookmarkedTips();
}
if (pageType === 'tags_search') {
translateTagSearchTips();
}
if (pageType === 'users_stats') {
translateStatsChart();
}
}
/**
* 更新页面设置
*/
function updatePageConfig() {
const newType = detectPageType();
if (newType && newType !== pageConfig.currentPageType) {
pageConfig = buildPageConfig(newType);
} else if (!pageConfig.currentPageType && newType) {
pageConfig = buildPageConfig(newType);
}
}
/**
* 构建页面设置 pageConfig 对象
*/
function buildPageConfig(pageType = pageConfig.currentPageType) {
const inheritanceMap = {
'admin_posts_index': 'admin_posts_show'
};
const effectivePageType = inheritanceMap[pageType] || pageType;
const baseStatic = I18N[CONFIG.LANG]?.public?.static || {};
const baseRegexp = I18N[CONFIG.LANG]?.public?.regexp || [];
const baseSelector = I18N[CONFIG.LANG]?.public?.selector || [];
const baseInnerHTMLRegexp = I18N[CONFIG.LANG]?.public?.innerHTML_regexp || [];
const globalFlexible = (effectivePageType === 'admin_posts_show') ? {} : (I18N[CONFIG.LANG]?.flexible || {});
const usersCommonStatic = (pageType.startsWith('users_') || pageType === 'profile' || pageType === 'dashboard')
? I18N[CONFIG.LANG]?.users_common?.static || {}
: {};
const pageStatic = I18N[CONFIG.LANG]?.[effectivePageType]?.static || {};
const pageRegexp = I18N[CONFIG.LANG]?.[effectivePageType]?.regexp || [];
const pageSelector = I18N[CONFIG.LANG]?.[effectivePageType]?.selector || [];
const pageInnerHTMLRegexp = I18N[CONFIG.LANG]?.[effectivePageType]?.innerHTML_regexp || [];
let pageFlexible = (effectivePageType === 'admin_posts_show') ? {} : (I18N[CONFIG.LANG]?.[effectivePageType]?.flexible || {});
const parentPageMap = {
'works_edit': 'works_new',
'works_edit_tags': 'works_new',
'chapters_new': 'works_new',
'chapters_edit': 'chapters_new',
'works_edit_multiple': 'works_new',
'skins_edit': 'skins'
};
const parentPageType = parentPageMap[pageType];
let extraStatic = {}, extraRegexp = [], extraSelector = [], extraInnerHTMLRegexp = [], extraFlexible = {};
if (parentPageType) {
const parentConfig = I18N[CONFIG.LANG]?.[parentPageType];
if (parentConfig) {
const parentFullConfig = buildPageConfig(parentPageType);
extraStatic = parentFullConfig.staticDict;
extraRegexp = parentFullConfig.regexpRules;
extraSelector = parentFullConfig.tranSelectors;
extraInnerHTMLRegexp = parentFullConfig.innerHTMLRules;
extraFlexible = { ...parentFullConfig.globalFlexibleDict, ...parentFullConfig.pageFlexibleDict };
}
}
const mergedStatic = { ...baseStatic, ...usersCommonStatic, ...extraStatic, ...pageStatic };
const mergedRegexp = [...pageRegexp, ...extraRegexp, ...baseRegexp];
const mergedSelector = [...pageSelector, ...extraSelector, ...baseSelector];
const mergedInnerHTMLRegexp = [...pageInnerHTMLRegexp, ...extraInnerHTMLRegexp, ...baseInnerHTMLRegexp].sort((a, b) => {
const getLength = (r) => {
return (r[1] instanceof RegExp) ? r[1].source.length : String(r[1]).length;
};
return getLength(b) - getLength(a);
});
const mergedPageFlexible = { ...extraFlexible, ...pageFlexible };
return {
currentPageType: pageType,
staticDict: mergedStatic,
regexpRules: mergedRegexp,
innerHTMLRules: mergedInnerHTMLRegexp,
globalFlexibleDict: globalFlexible,
pageFlexibleDict: mergedPageFlexible,
ignoreMutationSelectors: [
...(I18N.conf.ignoreMutationSelectorPage['*'] || []),
...(I18N.conf.ignoreMutationSelectorPage[pageType] || [])
].join(', ') || ' ',
ignoreSelectors: [
...(I18N.conf.ignoreSelectorPage['*'] || []),
...(I18N.conf.ignoreSelectorPage[pageType] || [])
].join(', ') || ' ',
characterData: I18N.conf.characterDataPage.includes(pageType),
tranSelectors: mergedSelector,
};
}
/**
* 页面类型检测
*/
function detectPageType() {
if (document.title.includes("You're clicking too fast!")) {
const h2 = document.querySelector('main h2');
if (h2 && h2.textContent.includes('Too many page requests too quickly')) {
return 'error_too_many_requests';
}
}
if (document.querySelector('ul.media.fandom.index.group')) return 'media_index';
if (document.querySelector('div#main.owned_tag_sets-show')) return 'owned_tag_sets_show';
const { pathname } = window.location;
if (pathname.startsWith('/first_login_help')) {
return false;
}
if (pathname === '/abuse_reports/new' || pathname === '/support') return 'report_and_support_page';
if (pathname === '/known_issues') return 'known_issues_page';
if (pathname === '/tos') return 'tos_page';
if (pathname === '/content') return 'content_policy_page';
if (pathname === '/privacy') return 'privacy_policy_page';
if (pathname === '/dmca') return 'dmca_policy_page';
if (pathname === '/tos_faq') return 'tos_faq_page';
if (pathname === '/abuse_reports/new') return 'abuse_reports_new';
if (pathname === '/support') return 'support_page';
if (pathname === '/diversity') return 'diversity_statement';
if (pathname === '/site_map') return 'site_map';
if (pathname.startsWith('/wrangling_guidelines')) return 'wrangling_guidelines_page';
if (pathname === '/donate') return 'donate_page';
if (pathname.startsWith('/faq')) return 'faq_page';
if (pathname === '/help/skins-basics.html') return 'help_skins_basics';
if (pathname === '/help/tagset-about.html') return 'help_tagset_about';
if (pathname === '/tag_sets') return 'tag_sets_index';
if (pathname === '/external_works/new') return 'external_works_new';
if (pathname === '/invite_requests' || pathname === '/invite_requests/status') return 'invite_requests_index';
const isSearchResultsPage = document.querySelector('h2.heading')?.textContent.trim() === 'Search Results';
if (pathname === '/works/search') {
return isSearchResultsPage ? 'works_search_results' : 'works_search';
}
if (pathname === '/people/search') {
return isSearchResultsPage ? 'people_search_results' : 'people_search';
}
if (pathname === '/bookmarks/search') {
return isSearchResultsPage ? 'bookmarks_search_results' : 'bookmarks_search';
}
if (pathname === '/tags/search') {
return isSearchResultsPage ? 'tags_search_results' : 'tags_search';
}
if (pathname === '/about') return 'about_page';
const pathSegments = pathname.substring(1).split('/').filter(Boolean);
if (pathname === '/users/login') return 'session_login';
if (pathname === '/users/logout') return 'session_logout';
if (pathname === '/') {
return document.body.classList.contains('logged-in') ? 'dashboard' : 'front_page';
}
if (pathSegments.length > 0) {
const p1 = pathSegments[0];
const p2 = pathSegments[1];
const p3 = pathSegments[2];
const p4 = pathSegments[3];
const p5 = pathSegments[4];
switch (p1) {
case 'admin_posts':
if (p2 && /^\d+$/.test(p2)) {
return 'admin_posts_show';
}
return 'admin_posts_index';
case 'comments':
if (document.querySelector('a[href="/admin_posts"]')) {
return 'admin_posts_show';
}
break;
case 'media':
return 'media_index';
case 'users':
if (p2 && p3 === 'pseuds') {
if (p4 === 'new') return 'users_settings';
if (p4) {
if (p5 === 'works') return 'users_works_index';
if (p5 === 'bookmarks') return 'users_bookmarks_index';
if (p5 === 'series') return 'users_series_index';
if (p5 === 'gifts') return 'users_gifts_index';
if (p5 === 'edit') return 'users_settings';
if (p5 === 'orphan') return 'orphans_new';
if (!p5) return 'profile';
}
if (!p4) return 'users_settings';
}
if (p2 && p3 === 'pseuds' && p5 === 'works') return 'users_works_index';
if (p2 && (p3 === 'blocked' || p3 === 'muted') && p4 === 'users') return 'users_block_mute_list';
if (p2 && p3 === 'dashboard') return 'dashboard';
if (p2 && p3 === 'profile' && p4 === 'edit') return 'users_settings';
if (p2 && p3 === 'profile') return 'profile';
if (p2 && p3 === 'stats') return 'users_stats';
if (p2 && p3 === 'readings') return 'users_history';
if (p2 && p3 === 'preferences') return 'preferences';
if (p2 && p3 === 'edit') return 'users_settings';
if (p2 && p3 === 'change_username') return 'users_settings';
if (p2 && p3 === 'change_password') return 'users_settings';
if (p2 && p3 === 'change_email') return 'users_settings';
if (p2 && p3 === 'works' && p4 === 'drafts') return 'users_drafts_index';
if (p2 && p3 === 'series') return 'users_series_index';
if (p2 && p3 === 'works' && p4 === 'show_multiple') return 'works_show_multiple';
if (p2 && p3 === 'works' && p4 === 'edit_multiple') return 'works_edit_multiple';
if (p2 && p3 === 'works') return 'users_works_index';
if (p2 && p3 === 'bookmarks') return 'users_bookmarks_index';
if (p2 && p3 === 'collections') return 'users_collections_index';
if (p2 && p3 === 'subscriptions') return 'users_subscriptions_index';
if (p2 && p3 === 'related_works') return 'users_related_works_index';
if (p2 && p3 === 'gifts') return 'users_gifts_index';
if (p2 && p3 === 'history') return 'users_history';
if (p2 && p3 === 'inbox') return 'users_inbox';
if (p2 && p3 === 'signups') return 'users_signups';
if (p2 && p3 === 'assignments') return 'users_assignments';
if (p2 && p3 === 'claims') return 'users_claims';
if (p2 && p3 === 'invitations') return 'users_invitations';
if (p2 && !p3) return 'profile';
break;
case 'works':
if (document.querySelector('div#main.works-update')) return 'works_edit';
if (p2 === 'new') {
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.get('import') === 'true') {
return 'works_import';
}
return 'works_new';
}
if (p2 === 'search') return isSearchResultsPage ? 'works_search_results' : 'works_search';
if (p2 && /^\d+$/.test(p2)) {
if (p3 === 'chapters' && p4 === 'new') return 'chapters_new';
if (p3 === 'chapters' && p4 && /^\d+$/.test(p4) && p5 === 'edit') return 'chapters_edit';
if (p3 === 'edit_tags') return 'works_edit_tags';
if (p3 === 'edit') return 'works_edit';
if (!p3 || p3 === 'navigate' || (p3 === 'chapters' && p4)) return 'works_chapters_show';
}
if (!p2) return 'works_index';
break;
case 'chapters':
if (p2 && /^\d+$/.test(p2)) {
return 'works_chapters_show';
}
break;
case 'series':
if (p2 && /^\d+$/.test(p2)) return 'series_show';
if (!p2) return 'series_index';
break;
case 'orphans':
return 'orphans_new';
case 'collections':
if (p2 === 'new') {
return 'collections_new';
}
return 'collections_dashboard_common';
case 'tags':
if (p2) {
if (pathSegments.slice(-1)[0] === 'works') return 'tags_works_index';
return 'tags_show';
}
if (!p2) return 'tags_index';
break;
case 'tag_sets':
if (p2 === 'new') {
return 'tag_sets_new';
}
if (p3 === 'nominations' && p4 === 'new') {
return 'tag_sets_nominations_new';
}
break;
case 'skins':
if (p2 === 'new') return 'skins';
if (p2 && /^\d+$/.test(p2) && p3 === 'edit') return 'skins_edit';
if (p2 && /^\d+$/.test(p2)) return 'skins_show';
return 'skins';
case 'bookmarks':
if (p2 && /^\d+$/.test(p2) && p3 === 'new') return 'bookmarks_new_for_work';
if (p2 && /^\d+$/.test(p2)) return 'bookmarks_show';
if (!p2) return 'bookmarks_index';
break;
}
}
if (document.body.classList.contains('dashboard')) return 'dashboard';
if (document.querySelector('body.works.index')) return 'works_index';
if (document.querySelector('body.works.show, body.chapters.show')) return 'works_chapters_show';
const pathMatch = pathname.match(I18N.conf.rePagePath);
if (pathMatch && pathMatch[1]) {
let derivedType = pathMatch[1];
if (pathMatch[2]) derivedType += `_${pathMatch[2]}`;
if (I18N[CONFIG.LANG]?.[derivedType]) {
return derivedType;
}
}
return 'common';
}
/**
* traverseNode 函数:遍历指定的节点,并对节点进行翻译。
* @param {Node} rootNode - 需要遍历的节点。
*/
function traverseNode(rootNode) {
if (rootNode.nodeType === Node.TEXT_NODE) {
if (rootNode.nodeValue && rootNode.nodeValue.length <= 1000) {
if (rootNode.parentElement && rootNode.parentElement.closest(pageConfig.ignoreSelectors)) {
return;
}
transElement(rootNode, 'nodeValue');
}
return;
}
if (rootNode.nodeType === Node.ELEMENT_NODE && rootNode.closest(pageConfig.ignoreSelectors)) {
return;
}
const treeWalker = document.createTreeWalker(
rootNode,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
node => {
if (node.nodeType === Node.ELEMENT_NODE && node.closest(pageConfig.ignoreSelectors)) {
return NodeFilter.FILTER_REJECT;
}
if (node.nodeType === Node.TEXT_NODE && node.parentElement && node.parentElement.closest(pageConfig.ignoreSelectors)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
);
const handleElement = node => {
switch (node.tagName) {
case 'INPUT':
case 'TEXTAREA':
if (['button', 'submit', 'reset'].includes(node.type)) {
transElement(node.dataset, 'confirm');
transElement(node, 'value');
} else {
transElement(node, 'placeholder');
transElement(node, 'title');
}
break;
case 'OPTGROUP':
transElement(node, 'label');
break;
case 'BUTTON':
transElement(node, 'title');
transElement(node.dataset, 'confirm');
transElement(node.dataset, 'confirmText');
transElement(node.dataset, 'confirmCancelText');
transElement(node.dataset, 'disableWith');
break;
case 'A':
transElement(node, 'title');
transElement(node.dataset, 'confirm');
break;
case 'SPAN':
case 'DIV':
case 'P':
case 'LI':
case 'DD':
case 'DT':
case 'H1': case 'H2': case 'H3': case 'H4': case 'H5': case 'H6':
transElement(node, 'title');
break;
case 'IMG':
transElement(node, 'alt');
break;
default:
if (node.hasAttribute('aria-label')) transElement(node, 'ariaLabel');
if (node.hasAttribute('title')) transElement(node, 'title');
break;
}
};
const handleTextNode = node => {
if (node.nodeValue && node.nodeValue.length <= 1000) {
transElement(node, 'nodeValue');
}
};
const handlers = {
[Node.ELEMENT_NODE]: handleElement,
[Node.TEXT_NODE]: handleTextNode
};
let currentNode;
while ((currentNode = treeWalker.nextNode())) {
handlers[currentNode.nodeType]?.(currentNode);
}
}
/**
* transTitle 函数:翻译页面标题。
*/
function transTitle() {
const text = document.title;
let translatedText = pageConfig.staticDict?.[text] || I18N[CONFIG.LANG]?.public?.static?.[text] || I18N[CONFIG.LANG]?.title?.static?.[text] || '';
if (!translatedText) {
const titleRegexRules = [
...(I18N[CONFIG.LANG]?.title?.regexp || []),
...(pageConfig.regexpRules || [])
];
for (const rule of titleRegexRules) {
if (!Array.isArray(rule) || rule.length !== 2) continue;
const [pattern, replacement] = rule;
if (pattern.test(text)) {
translatedText = text.replace(pattern, replacement);
if (translatedText !== text) break;
}
}
}
if (translatedText && translatedText !== text) {
document.title = translatedText;
}
}
/**
* transElement 函数:翻译指定元素的文本内容或属性。
*/
function transElement(el, field) {
if (!el || !el[field]) return false;
const text = el[field];
if (typeof text !== 'string' || !text.trim()) return false;
const translatedText = transText(text, el);
if (translatedText && translatedText !== text) {
try {
el[field] = translatedText;
} catch (e) {
}
}
}
/**
* transText 函数:翻译文本内容。
*/
function transText(text, el) {
if (!text || typeof text !== 'string') return false;
const originalText = text;
let translatedText = text;
const applyFlexibleDict = (targetText, dict) => {
if (!dict) return targetText;
const keys = Object.keys(dict);
if (keys.length === 0) return targetText;
const regexParts = keys.map(key => {
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (/^[\w\s]+$/.test(key)) {
return `\\b${escapedKey}\\b`;
} else {
return escapedKey;
}
});
const flexibleRegex = new RegExp(`(${regexParts.join('|')})`, 'g');
if (el && el.nodeType === Node.TEXT_NODE && el.parentElement && el.parentElement.matches('h2.heading a.tag')) {
const fullTagText = el.parentElement.textContent.trim();
if (dict[fullTagText]) {
return targetText.replace(fullTagText, dict[fullTagText]);
} else {
return targetText;
}
}
return targetText.replace(flexibleRegex, (matched) => dict[matched] || matched);
};
translatedText = applyFlexibleDict(translatedText, pageConfig.pageFlexibleDict);
translatedText = applyFlexibleDict(translatedText, pageConfig.globalFlexibleDict);
const staticDict = pageConfig.staticDict || {};
const trimmedText = translatedText.trim();
if (staticDict[trimmedText]) {
translatedText = translatedText.replace(trimmedText, staticDict[trimmedText]);
}
if (FeatureSet.enable_RegExp && pageConfig.regexpRules) {
for (const rule of pageConfig.regexpRules) {
if (!Array.isArray(rule) || rule.length !== 2) continue;
const [pattern, replacement] = rule;
if (pattern.test(translatedText)) {
if (typeof replacement === 'function') {
translatedText = translatedText.replace(pattern, replacement);
} else {
translatedText = translatedText.replace(pattern, replacement);
}
}
}
}
return translatedText !== originalText ? translatedText : false;
}
/**
* transBySelector 函数:通过 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译。
*/
function transBySelector() {
if (!pageConfig.tranSelectors) return;
pageConfig.tranSelectors.forEach(rule => {
if (!Array.isArray(rule) || rule.length !== 2) return;
const [selector, translatedText] = rule;
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
if (element && element.textContent !== translatedText) {
element.textContent = translatedText;
}
});
} catch (e) {
}
});
}
/**
* 主翻译入口函数
*/
function transDesc() {
if (!FeatureSet.enable_transDesc) {
return;
}
const universalRules = [
{ selector: 'blockquote.userstuff.summary', text: '翻译简介' },
{ selector: '.summary > blockquote.userstuff', text: '翻译简介' },
{ selector: 'blockquote.userstuff.notes', text: '翻译注释' },
{ selector: '.notes > blockquote.userstuff', text: '翻译注释' },
{ selector: '.comment blockquote.userstuff', text: '翻译评论' },
{ selector: 'div.bio > div.userstuff', text: '翻译简介' },
{ selector: '.pseud blockquote.userstuff', text: '翻译简介' },
{ selector: '.latest.news .post.group > blockquote.userstuff', text: '翻译概述' },
{ selector: 'dl.work.meta.group', text: '翻译标签', above: false, isTags: true },
{ selector: 'ul.tags.commas', text: '翻译标签', above: false, isTags: true },
{ selector: '#admin-banner blockquote.userstuff', text: '翻译公告', insertInside: true },
];
const pageSpecificRules = {
'works_show': [
{ selector: '#chapters > .userstuff', text: '翻译正文', above: true, isLazyLoad: true },
{ selector: '#chapters > .chapter > .userstuff[role="article"]', text: '翻译正文', above: true, isLazyLoad: true }
],
'works_chapters_show': [
{ selector: '#chapters > .userstuff', text: '翻译正文', above: true, isLazyLoad: true },
{ selector: '#chapters > .chapter > .userstuff[role="article"]', text: '翻译正文', above: true, isLazyLoad: true }
],
'collections_dashboard_common': [
{ selector: '.primary.header.module blockquote.userstuff', text: '翻译概述', above: false, isLazyLoad: false },
{ selector: '#intro blockquote.userstuff', text: '翻译简介', above: false, isLazyLoad: false }
],
'admin_posts_show': [
{ selector: 'div[role="article"] > .userstuff', text: '翻译动态', above: true, isLazyLoad: true }
],
'admin_posts_index': [
{ selector: '.admin_posts-index div[role="article"] > .userstuff', text: '翻译动态', above: true, isLazyLoad: true }
],
'tos_page': [
{ selector: '#tos.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'content_policy_page': [
{ selector: '#content.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'privacy_policy_page': [
{ selector: '#privacy.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'dmca_policy_page': [
{ selector: '#DMCA.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'tos_faq_page': [
{ selector: '.admin.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'wrangling_guidelines_page': [
{ selector: '.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'faq_page': [
{ selector: '.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'known_issues_page': [
{ selector: '.admin.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
],
'report_and_support_page': [
{ selector: '.userstuff', text: '翻译内容', above: true, isLazyLoad: true }
]
};
const applyRules = (rules) => {
rules.forEach(rule => {
if (rule.selector === 'ul.tags.commas' && (pageConfig.currentPageType === 'admin_posts_show' || pageConfig.currentPageType === 'admin_posts_index' || pageConfig.currentPageType === 'collections_dashboard_common')) {
return;
}
document.querySelectorAll(rule.selector).forEach(element => {
if (element.dataset.translationHandled) return;
if (element.textContent.trim() === '') return;
if (rule.isLazyLoad && element.closest('.summary, .notes, .comment')) return;
if (element.classList.contains('translated-tags-container') || element.closest('.translated-tags-container')) return;
const blurbContainer = element.closest('.blurb.group');
if (rule.selector === 'ul.tags.commas' && blurbContainer) {
const hasSummary = blurbContainer.querySelector('blockquote.userstuff.summary, .summary > blockquote.userstuff');
if (hasSummary) return;
}
let linkedTagsNode = null;
if (blurbContainer && !element.classList.contains('tags') && (element.classList.contains('summary') || element.closest('.summary'))) {
linkedTagsNode = blurbContainer.querySelector('ul.tags.commas');
}
addTranslationButton(element, rule.text, rule.above || false, rule.isLazyLoad || false, rule.isTags || false, linkedTagsNode, rule.insertInside || false);
});
});
};
applyRules(universalRules);
const currentSpecificRules = pageSpecificRules[pageConfig.currentPageType];
if (currentSpecificRules) {
applyRules(currentSpecificRules);
}
}
/**
* 为指定元素添加翻译按钮
*/
function addTranslationButton(element, originalButtonText, isAbove, isLazyLoad, isTags, linkedTagsNode = null, insertInside = false) {
element.dataset.translationHandled = 'true';
const wrapper = document.createElement('div');
wrapper.className = 'translate-me-ao3-wrapper state-idle';
if (isTags) {
wrapper.classList.add('type-tags');
}
const buttonLink = document.createElement('div');
buttonLink.className = 'translate-me-ao3-button';
buttonLink.textContent = originalButtonText;
wrapper.appendChild(buttonLink);
if (isTags && element.tagName === 'DL' && element.classList.contains('meta') && element.parentElement.classList.contains('wrapper')) {
if (isAbove) {
element.parentElement.prepend(wrapper);
} else {
element.parentElement.after(wrapper);
}
} else {
if (insertInside) {
element.appendChild(wrapper);
} else if (isAbove) {
element.prepend(wrapper);
} else {
element.after(wrapper);
}
}
let controller;
if (linkedTagsNode) {
controller = createBlurbTranslationController({
summaryElement: element,
tagsElement: linkedTagsNode,
buttonWrapper: wrapper,
originalButtonText: originalButtonText
});
} else if (isTags) {
controller = createTagsTranslationController({
containerElement: element,
buttonWrapper: wrapper,
originalButtonText: originalButtonText
});
} else {
controller = createTranslationController({
containerElement: element,
buttonWrapper: wrapper,
originalButtonText: originalButtonText,
isLazyLoad: isLazyLoad
});
}
buttonLink.addEventListener('click', () => controller.handleClick());
}
/**
* fetchTranslatedText 函数:从特定页面的词库中获得翻译文本内容。
* @param {string} text - 需要翻译的文本内容
* @returns {string|boolean} 翻译后的文本内容
*/
function fetchTranslatedText(text) {
if (pageConfig.staticDict && pageConfig.staticDict[text] !== undefined) {
return pageConfig.staticDict[text];
}
if (FeatureSet.enable_RegExp && pageConfig.regexpRules) {
for (const rule of pageConfig.regexpRules) {
if (!Array.isArray(rule) || rule.length !== 2) continue;
const [pattern, replacement] = rule;
if (pattern instanceof RegExp && pattern.test(text)) {
const translated = text.replace(pattern, replacement);
if (translated !== text) return translated;
} else if (typeof pattern === 'string' && text.includes(pattern)) {
const translated = text.replace(new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replacement);
if (translated !== text) return translated;
}
}
}
return false;
}
/**
* 翻译标题中的标签
*/
function translateHeadingTags() {
const headingTags = document.querySelectorAll('h2.heading a.tag');
if (headingTags.length === 0) return;
const fullDictionary = {
...pageConfig.staticDict,
...pageConfig.globalFlexibleDict,
...pageConfig.pageFlexibleDict
};
headingTags.forEach(tagElement => {
if (tagElement.hasAttribute('data-translated-by-custom-function')) {
return;
}
const originalText = tagElement.textContent.trim();
if (fullDictionary[originalText]) {
tagElement.textContent = fullDictionary[originalText];
}
tagElement.setAttribute('data-translated-by-custom-function', 'true');
});
}
/**************************************************************************
* I18N 翻译数据区
**************************************************************************/
const I18N = {
'conf': {
ignoreMutationSelectorPage: {
'*': ['.userstuff .revised.at', '.kudos_count', '.bookmark_count', '.comment_count', '.hit_count', '.view_count'],
'works_show': ['.stats .hits', '.stats .kudos'],
},
ignoreSelectorPage: {
'*': ['script', 'style', 'noscript', 'iframe', 'canvas', 'video', 'audio', 'img', 'svg', 'pre', 'code', '.userstuff.workskin', '.workskin', 'div.autocomplete.dropdown ul', 'dd.freeform.tags', '[data-translated-by-custom-function]', 'li.freeforms', 'blockquote.userstuff.summary', 'textarea#embed_code', '.header.module h4.heading a[href^="/series/"]', '.header.module h4.heading a[href^="/works/"]', '.header.module h4.heading a[rel="author"]', '.header.module h5.fandoms a.tag', 'ul.series a[href^="/series/"]', 'dd.series a[href^="/series/"]'],
'works_show': ['.dropdown.actions-menu ul', '#main .userstuff'],
'works_chapters_show': ['#main .userstuff'],
'series_show': ['h2.heading'],
'admin_posts_show': ['.userstuff'],
'tag_sets_index': ['h2.heading', 'dl.stats'],
'tag_sets_new': ['h4.heading > label[for*="freeform"]'],
'collections_dashboard_common': ['.primary.header.module blockquote.userstuff'],
'faq_page': ['.userstuff', '.faq.index.group'],
'wrangling_guidelines_page': ['.userstuff'],
'tos_page': ['#tos.userstuff'],
'content_policy_page': ['#content.userstuff'],
'privacy_policy_page': ['#privacy.userstuff'],
'dmca_policy_page': ['#DMCA.userstuff'],
'tos_faq_page': ['.admin.userstuff'],
'abuse_reports_new': ['.userstuff'],
'support_page': ['.userstuff'],
'known_issues_page': ['.admin.userstuff'],
'report_and_support_page': ['.userstuff'],
},
characterDataPage: ['common', 'works_show', 'users_dashboard'],
rePagePath: /^\/([a-zA-Z0-9_-]+)(?:\/([a-zA-Z0-9_-]+))?/
},
'zh-CN': {
'title': {
'static': {},
'regexp': []
},
'public': {
'static': {
// 基本
'Archive of Our Own': 'AO3 作品库',
'Fandoms': '同人圈', 'All Fandoms': '所有同人圈',
'Browse': '浏览', 'Works': '作品', 'Bookmarks': '书签', 'Tags': '标签', 'Collection': '合集', 'Collections': '合集',
'Search': '搜索', 'People': '用户',
'About': '关于', 'About Us': '关于我们', 'News': '新的动态', 'FAQ': '常见问题', 'Wrangling Guidelines': '整理指南', 'Donate or Volunteer': '捐赠/志愿',
'Recent Works': '最近作品',
'Recent Series': '最近系列',
'Recent Bookmarks': '最近书签', 'Collections:': '合集:',
'Bookmarker\'s Tags:': '创建者的标签:', 'Bookmarker\'s Collections:': '创建者的合集:', 'Completed': '已完结',
'Bookmark Tags:': '书签标签:', 'Complete Work': '已完结', 'Work in Progress': '连载中', 'Public Bookmark': '公开书签',
'Most Popular': '最常用', 'Tag Sets': '标签集',
'Warnings': '预警',
'Find your favorites': '寻找喜欢的内容',
// 登录
'Log In': '登录',
'Log in': '登录',
'Sign Up': '注册',
'User': '用户',
'Username or email:': '用户名或邮箱:',
'Password:': '密码:',
'Remember Me': '记住我',
'Remember me': '记住我',
'Forgot password?': '忘记密码?',
'Get an Invitation': '获取邀请',
// 忘记密码
'Forgotten your password?': '忘记您的密码了吗?',
'If you\'ve forgotten your password, we can send instructions that will allow you to reset it. Please tell us the username or email address you used when you signed up for your Archive account.': '如果您忘记了密码,我们可以发送允许您重置密码的邮件说明。请输入您注册 AO3 帐户时使用的用户名或电子邮箱地址。',
'Reset Password': '重置密码',
// 星期
'Mon': '周一',
'Tue': '周二',
'Wed': '周三',
'Thu': '周四',
'Fri': '周五',
'Sat': '周六',
'Sun': '周日',
'Monday': '星期一',
'Tuesday': '星期二',
'Wednesday': '星期三',
'Thursday': '星期四',
'Friday': '星期五',
'Saturday': '星期六',
'Sunday': '星期日',
// 月份
'Jan': '1月',
'Feb': '2月',
'Mar': '3月',
'Apr': '4月',
'May': '5月',
'Jun': '6月',
'Jul': '7月',
'Aug': '8月',
'Sep': '9月',
'Oct': '10月',
'Nov': '11月',
'Dec': '12月',
'January': '1月',
'February': '2月',
'March': '3月',
'April': '4月',
'May': '5月',
'June': '6月',
'July': '7月',
'August': '8月',
'September': '9月',
'October': '10月',
'November': '11月',
'December': '12月',
// 页脚
'Footer': '页脚',
'Customize': '自定义',
'Default': '默认界面',
'Low Vision Default': '低视力默认界面',
'Reversi': 'Reversi 界面',
'Snow Blue': 'Snow Blue 界面',
'About the Archive': '关于 Archive',
'Site Map': '站点地图',
'Diversity Statement': '多元化声明',
'Terms of Service': '服务条款',
'Content Policy': '内容政策',
'Privacy Policy': '隐私政策',
'DMCA Policy': 'DMCA 政策',
'Site Status': '站点状态',
'TOS FAQ': '服务条款常见问题',
'↑ Top': '↑ 回到顶部',
'Frequently Asked Questions': '常见问题',
'Contact Us': '联系我们',
'Policy Questions & Abuse Reports': '政策咨询与滥用举报',
'Technical Support & Feedback': '技术支持与反馈',
'Development': '开发',
'Known Issues': '已知问题',
'View License': '查看许可证',
'OTW': 'OTW',
'Organization for Transformative Works': '再创作组织',
// 反馈
'Support and Feedback': '支持与反馈',
'FAQs & Tutorials': '常见问题与教程',
'Release Notes': '更新日志',
// 动态
'News': '最新动态',
'All News': '全部动态',
'Published': '发布于',
'Comments': '评论',
'Read more...': '更多',
'Tag:': '标签:',
'Go': '确定',
'RSS Feed': 'RSS 订阅',
'Follow us': '关注我们',
'What\'s New': '新增内容',
'Enter Comment': '输入评论',
'Last Edited': '最后编辑',
// 同人圈
'Anime & Manga': '动漫及漫画', 'Books & Literature': '书籍及文学', 'Cartoons & Comics & Graphic Novels': '卡通,漫画及图像小说', 'Celebrities & Real People': '明星及真人', 'Movies': '电影', 'Music & Bands': '音乐及乐队', 'Other Media': '其她媒体', 'Theater': '戏剧', 'TV Shows': '电视剧', 'Video Games': '电子游戏', 'Uncategorized Fandoms': '未分类的同人圈',
'> Anime & Manga': ' > 动漫及漫画', '> Books & Literature': ' > 书籍及文学', '> Cartoons & Comics & Graphic Novels': ' > 卡通,漫画及图像小说', '> Celebrities & Real People': ' > 明星及真人', '> Movies': ' > 电影', '> Music & Bands': ' > 音乐及乐队', '> Other Media': ' > 其她媒体', '> Theater': ' > 戏剧', '> TV Shows': ' > 电视剧', '> Video Games': ' > 电子游戏', '> Uncategorized Fandoms': ' > 未分类的同人圈',
// 个人中心
'My Dashboard': '个人中心',
'My Subscriptions': '订阅列表',
'My History': '历史记录',
'My Preferences': '偏好设置',
'Dashboard': '仪表盘',
'Preferences': '偏好设置',
'Skins': '站点界面',
'Works in Collections': '合集中的作品',
'Drafts': '草稿',
'Please note:': '注意:',
'Unposted drafts are only saved for a month from the day they are first created, and then deleted from the Archive.': '未发布的草稿自创建日起仅保留一个月,之后将被从 Archive 中删除。',
'Series': '系列',
'Bookmark External Work': '为外部作品创建书签',
'Sorry, there were no collections found.': '抱歉,未找到任何合集。',
'Manage Collection Items': '管理合集',
'New Collection': '新建合集',
'Works in Challenges/Collections': '参与挑战/合集的作品',
'Awaiting Collection Approval': '等待合集方审核',
'Awaiting User Approval': '等待用户确认',
'Rejected by Collection': '合集方已拒绝',
'Rejected by User': '用户已拒绝',
'Approved': '已通过',
'Nothing to review here!': '当前无待审内容!',
'Inbox': '消息中心',
'Filter by read': '按阅读状态筛选',
'Show all': '显示全部',
'Show unread': '显示未读',
'Show read': '显示已读',
'Filter by replied to': '按回复状态筛选',
'Show all': '显示全部',
'Show without replies': '显示未回复',
'Show replied to': '显示已回复',
'Sort by date': '按日期排序',
'Newest first': '最新优先',
'Oldest first': '最早优先',
'Filter': '筛选',
'Statistics': '数据统计',
'History': '历史记录',
'Full History': '全部历史记录',
'Marked for Later': '稍后阅读',
'Is it later already?': '到“稍后”了吗?',
'Some works you\'ve marked for later.': '这里是您标记为稍后阅读的作品。',
'Clear History': '清空历史记录',
'Delete from History': '删除历史记录',
'Subscriptions': '订阅列表',
'All Subscriptions': '所有订阅',
'Series Subscriptions': '系列订阅',
'User Subscriptions': '用户订阅',
'Work Subscriptions': '作品订阅',
'My Series Subscriptions': '系列订阅',
'My User Subscriptions': '用户订阅',
'My Work Subscriptions': '作品订阅',
'Delete All Work Subscriptions': '删除所有作品订阅',
'Delete All Series Subscriptions': '删除所有系列订阅',
'Delete All User Subscriptions': '删除所有用户订阅',
'Yes, Delete All Subscriptions': '是的,删除所有订阅',
'Yes, Delete All Work Subscriptions': '是的,删除所有作品订阅',
'Yes, Delete All Series Subscriptions': '是的,删除所有系列订阅',
'Yes, Delete All User Subscriptions': '是的,删除所有用户订阅',
'Your subscriptions have been deleted.': '您的订阅已成功删除。',
'Unsubscribe': '取消订阅',
'Delete All Subscriptions': '删除所有订阅',
'Sign-ups': '报名挑战',
'Assignments': '任务中心',
'Unfulfilled Claims': '未完成的认领',
'Fulfilled Claims': '已完成的认领',
'Claims': '我的认领',
'Related Works': '相关作品',
'Gifts': '赠文',
'Accepted Gifts': '已接受的赠文',
'Refused Gifts': '已拒绝的赠文',
'Choices': '用户选项',
'Pitch': '创作与发布',
'Catch': '互动与追踪',
'Switch': '活动与交换',
'My Works': '我的作品',
'My Series': '我的系列',
'My Bookmarks': '我的书签',
'My Collections': '我的合集',
'History': '历史记录',
'Log Out': '登出',
'Post New': '发布新作',
'Edit Works': '编辑作品',
'Subscribe': '订阅',
'Invitations': '邀请',
'My pseuds:': '笔名:',
'Name (required)': '名称(必填)',
'Create Pseud': '创建笔名',
'Edit Pseud': '编辑笔名',
'Back To Pseuds': '返回笔名列表',
'Pseuds': '笔名',
'Pseud was successfully created.': '笔名已成功创建。',
'I joined on:': '加入于:',
'My user ID is:': '用户ID:',
'Edit My Works': '编辑作品',
'Edit My Profile': '编辑资料',
'Set My Preferences': '设置偏好',
'Manage My Pseuds': '管理笔名',
'Delete My Account': '删除账号',
'Blocked Users': '已屏蔽用户',
'Muted Users': '已静音用户',
'Change Username': '修改用户名',
'Change Password': '修改密码',
'Email address': '邮箱地址',
'Change Email': '修改邮箱',
'Privacy': '隐私设置',
'Show my email address to other people.': '向其她人显示我的邮箱地址',
'Show my date of birth to other people.': '向其她人显示我的出生日期',
'Hide my work from search engines when possible.': '尽可能地对搜索引擎隐藏我的作品',
'Hide the share buttons on my work.': '隐藏我作品中的分享按钮',
'Allow others to invite me to be a co-creator.': '允许其她人邀请我成为共同创作者',
'Display': '显示设置',
'Show me adult content without checking.': '无需确认即可显示成人内容',
'Show the whole work by default.': '默认显示全文',
'Hide warnings (you can still choose to show them).': '隐藏内容预警(仍可手动显示)',
'Hide additional tags (you can still choose to show them).': '隐藏附加标签(仍可手动显示)',
'Hide work skins (you can still choose to show them).': '隐藏作品界面(仍可手动显示)',
'Your site skin': '您的站点界面',
'Public Site Skins': '公共站点界面',
'Your time zone': '您所在的时区',
'Browser page title format': '浏览页面标题格式',
'Turn off emails about comments.': '关闭评论邮件通知',
'Turn off messages to your inbox about comments.': '关闭评论消息通知',
'Turn off copies of your own comments.': '关闭自己评论的副本通知',
'Turn off emails about kudos.': '关闭点赞邮件通知',
'Do not allow guests to reply to my comments on news posts or other users\' works (you can still control the comment settings for your works separately).': '不允许游客回复我在动态帖或其她用户作品中的评论(仍可单独调整自己作品的评论权限)',
'Collections, Challenges and Gifts': '合集、挑战与赠文设置',
'Allow others to invite my works to collections.': '允许其她人将我的作品加入合集',
'Allow anyone to gift me works.': '允许任何人向我赠送作品',
'Turn off emails from collections.': '关闭来自合集的邮件通知',
'Turn off inbox messages from collections.': '关闭来自合集的消息通知',
'Turn off emails about gift works.': '关闭有关赠文的邮件通知',
'Misc': '其她偏好设置',
'Turn on History.': '启用历史记录',
'Turn the new user help banner back on.': '重新显示新用户帮助横幅',
'Turn off the banner showing on every page.': '关闭每个页面的提示横幅',
'Update': '确定',
'My Site Skins': '我的站点界面',
'Create Site Skin': '创建站点界面',
'A site skin lets you change the way the Archive is presented when you are logged in to your account. You can use work skins to customize the way your own works are shown to others.': '站点界面可让您在登录账户后更改 Archive 的呈现方式。您也可以使用作品界面来自定义其她人查看您作品时的展示样式。',
'My Site Skins': '我的站点界面',
'My Work Skins': '我的作品界面',
'Public Work Skins': '公共作品界面',
'Create Work Skin': '创建作品界面',
'No site skins here yet!': '还没有站点界面!',
'No work skins here yet!': '还没有作品界面!',
'Why not try making one?': '为什么不试着去创建一个呢?',
'Inbox': '收件箱',
'Subscribed Works': '已订阅作品',
'Subscribed Series': '已订阅系列',
'Unposted Assignments': '未发布的任务',
'Completed Assignments': '已完成的任务',
// 合集管理与设置
'Creator/Pseud(s)': '创作者/笔名',
'Details ↓': '详情 ↓',
'Close Details ↑': '收起详情 ↑',
'Bookmarker approval status': '书签创建者审核状态',
'Unreviewed by bookmarker': '书签创建者未审核',
'Approved by bookmarker': '书签创建者已通过',
'Rejected by bookmarker': '书签创建者已拒绝',
'Collection approval status': '合集审核状态',
'Unreviewed by collection moderators': '合集管理员未审核',
'Approved by collection moderators': '合集管理员已通过',
'Rejected by collection moderators': '合集管理员已拒绝',
'Remove': '移除',
'Owner pseud(s)': '所有者笔名',
'Collection Tags': '合集标签',
'Collection tags:': '合集标签:',
'Enter up to 10 tags to describe the content of your collection.': '最多输入 10 个标签来描述您的合集内容。',
'Use this if your collection is not fandom-specific.': '如果您的合集不针对特定同人圈,请使用此项。',
// 作品搜索页
'Work Info': '作品信息',
'Date Posted': '发布日期',
'Date Updated': '更新日期',
'Completion status': '完成状态',
'All works': '所有作品',
'Complete works only': '仅完结作品',
'Works in progress only': '仅连载作品',
'Include crossovers': '包含跨圈作品',
'Exclude crossovers': '排除跨圈作品',
'Only crossovers': '仅限跨圈作品',
'Single Chapter': '单个章节',
'Rating': '分级',
'Categories': '分类',
'Other': '其她',
'Work Stats': '作品统计',
'Hits': '点击',
'Kudos': '点赞',
'Kudos ♥': '点赞 ♥',
'Sort by': '排序方式',
'Best Match': '最佳匹配',
'Sort direction': '排序方向',
'Descending': '降序',
'Ascending': '升序',
'Filter by title': '按标题筛选',
'Filter by tag': '按标签筛选',
'Work Search': '作品搜索',
'Any Field': '任意字段',
'Date': '日期',
'Crossovers': '跨圈作品',
'Language': '语言',
'Characters': '角色',
'Relationships': '关系',
'Additional Tags': '附加标签',
// 用户搜索页
'Search all fields': '搜索所有字段',
'Name': '名称',
'Fandom': '同人圈',
'Search People': '搜索用户',
// 标签搜索页
'Tag name': '标签名称',
'Find tags wrangled to specific canonical fandoms.': '查找已整理至特定规范同人圈的标签。',
'Type': '类型',
'Fandom': '同人圈',
'Character': '角色',
'Relationship': '关系',
'Freeform': '自由标签',
'Any type': '任意类型',
'Wrangling status': '整理状态',
'Canonical': '规范',
'Non-canonical': '非规范',
'Synonymous': '同义',
'Canonical or synonymous': '规范或同义',
'Non-canonical and non-synonymous': '非规范且非同义',
'Any status': '任意状态',
'Name': '名称',
'Date Created': '创建日期',
'Uses': '使用次数',
'Search Tags': '搜索标签',
'Title': '标题',
'Author': '作者',
'Artist': '画师',
'Author/Artist': '作者/画师',
'People Search': '用户搜索',
'Tag Search': '标签搜索',
'Work Tags': '作品标签',
// 浏览
'Expand Fandoms List': '展开同人圈列表',
'Collapse Fandoms List': '收起同人圈列表',
'Recent works': '最近作品',
'Recent series': '最近系列',
'Recent bookmarks': '最近书签',
'Expand Works List': '展开作品列表',
'Collapse Works List': '收起作品列表',
'Expand Bookmarks List': '展开书签列表',
'Collapse Booksmarks List': '收起书签列表',
// 个人资料
'Edit My Profile': '编辑简介',
'Edit Profile': '编辑简介',
'Edit Default Pseud and Icon': '编辑笔名和头像',
'Change Username': '更改用户名',
'Change My Username': '更改用户名',
'Change Password': '更改密码',
'Change My Password': '更改密码',
'Change Email': '更改邮箱',
'Title': '标题',
'Location': '位置',
'Date of Birth': '出生日期',
'About Me': '关于我',
'Plain text with limited HTML': '纯文本,支持有限 HTML',
'Embedded images (
tags) will be displayed as HTML, including the image\'s source link and any alt text.': '嵌入的图像(
标签)将显示为 HTML,包括图像的源链接和任何替代文本。',
'Update': '更新',
'Editing pseud': '编辑笔名',
'Name': '名称',
'Make this name default': '将此笔名设为默认',
'Description': '简介',
'Icon': '头像',
'This is your icon.': '这是您的头像。',
'You can have one icon for each pseud.': '每个笔名可设置一个头像。',
'Icons can be in png, jpeg or gif form.': '头像格式支持 PNG、JPEG 和 GIF。',
'Icons should be sized 100x100 pixels for best results.': '建议头像尺寸为 100×100 像素以获得最佳效果。',
'Upload a new icon': '上传新头像',
'Icon alt text': '头像替代文本',
'Icon comment text': '头像注释文本',
'New Pseud': '新建笔名',
'Default Pseud': '默认笔名',
'Edit Pseud': '编辑笔名',
'Edit': '编辑',
'Current username': '当前用户名',
'New username': '新用户名',
'Your username has been successfully updated.': '您的用户名已成功更新。',
'Password': '密码',
'New password': '新密码',
'Confirm new password': '确认新密码',
'Old password': '旧密码',
'Current email': '当前邮箱',
'New email': '新邮箱',
'Enter new email again': '再次输入新邮箱',
'Confirm New Email': '确认新邮箱',
'Submit': '提交',
'Create': '创建',
// 作品
'Rating:': '分级:',
'Archive Warning:': 'Archive 预警:',
'Archive Warnings:': 'Archive 预警:',
'Archive Warning': 'Archive 预警',
'Archive Warnings': 'Archive 预警',
'Category:': '分类:',
'Categories:': '分类:',
'Fandom:': '同人圈:',
'Fandoms:': '同人圈:',
'Relationship:': '关系:',
'Relationships:': '关系:',
'Character:': '角色:',
'Characters:': '角色:',
'Additional Tag:': '附加标签:',
'Additional Tags:': '附加标签:',
'Language:': '语言:',
'Series': '系列',
'Series:': '系列:',
'Stats:': '统计:',
'Published:': '发布于:',
'Completed:': '完结于:',
'Updated:': '更新于:',
'Words:': '字数:',
'Chapters:': '章节:',
'Comments:': '评论:',
'Kudos:': '点赞:',
'Bookmarks:': '书签:',
'Hits:': '点击:',
'Complete?': '已完结?',
'Word Count:': '字数:',
'Date Updated:': '更新日期:',
'Post': '发布',
'New Work': '新作品',
'Edit Work': '编辑作品',
'Import Work': '导入作品',
'From Draft': '从草稿',
'Edit': '编辑',
'Edit Tags': '编辑标签',
'Add Chapter': '添加章节',
'Post Draft': '发布草稿',
'Delete Draft': '删除草稿',
'Post Chapter': '发布章节',
'Edit Chapter': '编辑章节',
'Delete Chapter': '删除章节',
'Manage Chapters': '管理章节',
'Drag chapters to change their order.': '拖动章节以更改顺序。',
'Enter new chapter numbers.': '输入新的章节编号。',
'Update Positions': '更新顺序',
'Update': '更新',
'Delete': '删除',
'Cancel': '取消',
'Save': '保存',
'Saved': '已保存',
'Submit': '提交',
'Orphan Work': '匿名化作品',
'Orphan Works': '匿名化作品',
'Filters': '筛选器',
'Sort By': '排序方式',
'Random': '随机',
'Creator': '创作者',
'Date Updated': '更新日期',
'Word Count': '字数统计',
'Summary': '简介',
'Summary:': '简介:',
'Notes': '注释',
'Work Text': '作品正文',
'Chapter Index': '章节索引',
'Full-page index': '整页索引',
'Full-Page Index': '整页索引',
'Entire Work': '完整作品',
'Next Chapter': '下一章',
'Previous Chapter': '上一章',
'kudos': ' 个赞',
'bookmark': ' 条书签',
'comment': ' 条评论',
'← Previous': '← 上一页',
'Next →': '下一页 →',
'All fields are required. Your email address will not be published.': '所有字段均为必填。您的电子邮箱地址不会被公开。',
'Guest name': '访客名称',
'Guest email': '访客邮箱',
'Please enter your name.': '请输入您的名称',
'Please enter your email address.': '请输入您的电子邮箱地址',
'Hide Creator\'s Style': '隐藏创作者样式',
'Show Creator\'s Style': '显示创作者样式',
'top level comment': '主评论',
'Share Work': '分享作品',
'Restore From Last Unposted Draft?': '从上次未发布的草稿继续',
'Delete Work': '删除作品',
'Save As Draft': '存为草稿',
'Save Draft': '保存草稿',
'Post Work': '发布作品',
// 合集
'Collections in the Archive of Our Own': ' AO3 中的合集',
'Profile': '简介',
'Join': '加入',
'Leave': '退出',
'Open Challenges': '开放中的挑战',
'Open Collections': '开放中的合集',
'Closed Collections': '已截止的合集',
'Moderated Collections': '审核制合集',
'Unmoderated Collections': '非审核制合集',
'Unrevealed Collections': '未公开合集',
'Anonymous Collections': '匿名合集',
'Sort and Filter': '排序及筛选',
'Filter collections:': '筛选合集:',
'Filter by title or name': '按标题或名称筛选',
'Filter by fandom': '按同人圈筛选',
'Closed': '已截止',
'Multifandom': '跨圈',
'Yes': '是',
'No': '否',
'Either': '皆可',
'Collection Type': '合集类型',
'No Challenge': '无挑战',
'Any': '任意',
'Clear Filters': '清除筛选',
// 书签
'Bookmark Search': '书签搜索',
'Edit Bookmark': '编辑书签',
'Start typing for suggestions!': '开始输入以获取建议',
'Searching...': '搜索中…',
'(No suggestions found)': '未找到建议',
'Any field on work': '作品任意字段', 'Work tags': '作品标签', 'Type': '类型', 'Work': '作品', 'Work language': '作品语言', 'External Work': '外部作品', 'Date updated': '更新日期', 'Bookmark': '书签', 'Any field on bookmark': '书签任意字段', 'Bookmarker\'s tags': '书签创建者的标签', 'Bookmarker': '书签创建者', 'Bookmark type': '书签类型', 'Rec': '推荐', 'With notes': '含注释', 'Date Bookmarked': '书签创建日期', 'Date bookmarked': '书签创建日期', 'Search Bookmarks': '搜索书签',
'Search Results': '搜索结果', 'Edit Your Search': '修改搜索设置',
'Ratings': '分级',
'Include': '包括',
'Include Ratings': '包括分级',
'Other tags to include': '要包括的其她标签',
'Exclude': '排除',
'Other tags to exclude': '要排除的其她标签',
'More Options': '更多选项',
'Show only crossovers': '仅显示跨圈作品',
'Completion Status': '完成状态',
'Search within results': '在结果中搜索',
'Bookmarker\'s Tags': '书签创建者标签',
'Other work tags to include': '要包括的其她作品标签',
'Other bookmarker\'s tags to include': '要包括的其她书签创建者标签',
'Search bookmarker\'s tags and notes': '搜索书签创建者标签和注释',
'Other work tags to exclude': '要排除的其她作品标签',
'Other bookmarker\'s tags to exclude': '要排除的其她书签创建者标签',
'Bookmark types': '书签类型',
'Recs only': '仅推荐',
'Only bookmarks with notes': '仅含注释',
'All Bookmarks': '所有书签',
'Add To Collection': '添加到合集',
'Share': '分享',
'Private Bookmark': '私人书签',
'Your tags': '标签',
'Plain text with limited HTML': '纯文本,支持有限 HTML',
'The creator\'s tags are added automatically.': '创建者的标签会自动添加',
'Comma separated, 150 characters per tag': '以逗号分隔,每个标签最多 150 字符',
'Add to collections': '添加到合集',
'Private bookmark': '私人书签',
'Create': '创建',
'Bookmark was successfully deleted.': '书签已成功删除。',
'Add Bookmark to collections': '将书签添加到合集',
'Collection name(s):': '合集名称:',
'collection name': '合集名称',
'Add': '添加',
'Back': '返回',
'Bookmark was successfully updated.': '书签已成功更新。',
'Share Bookmark': '分享书签',
'Close': '关闭',
'Show': '展示',
'Bookmark Collections:': '书签合集:',
// 系列
'Creators:': '创建者:',
'Creator:': '创建者:',
'Series Begun:': '系列开始于:',
'Series Updated:': '系列更新于:',
'Description:': '描述:',
'Notes:': '注释:',
'Works:': '作品:',
'Complete:': '完结:',
// 语言
'Work Languages': '作品语言',
'Suggest a Language': '建议语言',
// 界面
'You are now using the default Archive skin again!': '您已重新切换至 Archive 默认界面!',
'Revert to Default Skin': '恢复默认界面',
'Role:': '功能:',
'user': '用户',
'Media:': '媒体:',
'all': '全部',
'Condition:': '状态:',
'Normal': '正常',
'(No Description Provided)': '(未提供描述)',
'Parent Skins': '母级界面',
'Use': '使用',
'Stop Using': '停用',
'Preview': '预览',
'Set For Session': '为当前会话设置',
'override': '覆盖',
// 屏蔽与静音
'Block': '屏蔽',
'Unblock': '取消屏蔽',
'Mute': '静音',
'Unmute': '取消静音',
'Yes, Unmute User': '是的,取消静音',
'Yes, Mute User': '是的,静音用户',
'Yes, Unblock User': '是的,取消屏蔽',
'Yes, Block User': '是的,屏蔽用户',
// 提示信息
'Your profile has been successfully updated': '您的个人资料已成功更新。',
'Your edits were put through! Please check over the works to make sure everything is right.': '您的编辑已生效!请检查相关作品,确保所有更改都已正确应用。',
'We\'re sorry! Something went wrong.': '非常抱歉!操作未完成,请稍后重试。',
'Your preferences were successfully updated.': '您的偏好设置已成功更新。',
'Works and bookmarks listed here have been added to a collection but need approval from a collection moderator before they are listed in the collection.': '此处列出的作品和书签已添加至合集中,但需经合集管理员批准后才会在合集内显示。',
'Successfully logged out.': '已成功登出。',
'Successfully logged in.': '已成功登录。',
'Bookmark was successfully created. It should appear in bookmark listings within the next few minutes.': '书签已创建成功。它将在接下来的几分钟内出现在书签列表中。',
'Browse fandoms by media or favorite up to 20 tags to have them listed here!': '可按媒体浏览同人圈,或收藏最多 20 个标签以在此展示。',
'You can search this page by pressing': '按', 'ctrl F': ' Ctrl + F ', 'cmd F': ' Cmd + F ,', '': '', 'and typing in what you are looking for.': '输入关键词即可在本页搜索。',
'Sorry! We couldn\'t save this bookmark because:': '抱歉!我们无法保存此书签,因为', 'Pseud can\'t be blank': '笔名不能为空',
'The following challenges are currently open for sign-ups! Those closing soonest are at the top.': '以下挑战现已开放报名!即将截止的挑战排在最前面。',
'You currently have no works posted to the Archive. If you add some, you\'ll find information on this page about hits, kudos, comments, and bookmarks of your works.': '您当前没有任何已发布的作品。添加作品后,您可以在此页面查看作品的访问量、点赞、评论和书签情况。',
'Users can also see how many subscribers they have, but not the names of their subscribers or identifying information about other users who have viewed or downloaded their works.': '用户还可以查看自己的订阅者数量,但无法看到订阅者的姓名,也无法获取浏览或下载其作品的其她用户的任何身份信息。',
'This work could have adult content. If you continue, you have agreed that you are willing to see such content.': '此作品可能含有成人内容。若您选择“继续”,即表示您同意查看此类内容。',
'Yes, Continue': '是,继续',
'No, Go Back': '否,返回',
'Set your preferences now': '设置偏好',
'Work successfully deleted from your history.': '该作品已成功从您的历史记录中删除。',
'Your history is now cleared.': '您的历史记录已清除。',
'You are already signed in.': '您已登录。',
'There are no works or bookmarks under this name yet.': '此名称下尚无作品或书签。',
'Sorry, you don\'t have permission to access the page you were trying to reach. Please log in.': '抱歉,您无权访问目标页面。请先登录。',
'Are you sure you want to delete this draft?': '您确定要删除此草稿吗?',
'Work was successfully updated.': '作品已成功更新。',
'The work was not updated.': '作品没有更新。',
'Your changes have not been saved. Please post your work or save as draft if you want to keep them.': '您的更改尚未保存。如果您想保留,请发布作品或将其保存为草稿。',
'Work was successfully posted. It should appear in work listings within the next few minutes.': '作品已成功发布。它将在接下来的几分钟内出现在作品列表中。',
'Are you sure you want to delete this work? This will destroy all comments and kudos on this work as well and CANNOT BE UNDONE!': '您确定要删除这篇作品吗?此操作将一并删除该作品收到的所有评论和点赞,且无法撤销!',
'Chapter has been posted!': '章节已成功发布!',
'Chapter was successfully updated.': '章节已成功更新。',
'Are you sure?': '您确定吗?',
'The chapter was successfully deleted.': '已成功删除此章节。',
'Chapter order has been successfully updated.': '章节顺序已成功更新。',
'This is a draft chapter in a posted work. It will be kept unless the work is deleted.': '这是已发布作品中的一篇草稿章节。除非作品被删除,否则该草稿将一直保留。',
'This chapter is a draft and hasn\'t been posted yet!': '本章节为草稿,尚未发布!',
'Are you sure you want to delete this bookmark?': '您确定要删除此书签吗?',
'This is part of an ongoing challenge and will be revealed soon!': '本作品正在参与一项开放中的挑战,内容将很快揭晓!',
'Your search failed because of a syntax error. Please try again.': '搜索失败,您的查询存在语法错误。请修改后重试。',
'Type or paste formatted text.': '输入或粘贴带有格式的文本',
'Comment created!': '评论已发布!',
'Are you sure you want to delete this comment?': '您确定要删除这条评论吗?',
'Yes, delete!': '是的,删除!',
'Comment deleted.': '评论已删除。',
'(Previous comment deleted.)': '(原评论已删除)',
'Freeze Thread': '锁定评论串',
'Comment thread successfully frozen!': '已成功锁定评论串!',
'Unfreeze Thread': '解锁评论串',
'Comment thread successfully unfrozen!': '已成功解锁评论串!',
'Frozen': '已锁定',
'Comment was successfully updated.': '评论已成功更新。',
'Sorry! We couldn\'t save this skin because:': '抱歉!我们无法保存此界面,因为:',
'Title must be unique': '标题必须唯一',
'We couldn\'t find any valid CSS rules in that code.': '代码中不存在任何有效的 CSS 规则',
'Skin was successfully created.': '界面已成功创建。',
'Skin was successfully updated.': '界面已成功删除。',
'Are you sure you want to delete this skin?': '您确定要删除此界面吗?',
'The skin was deleted.': '界面已删除。',
'Your changes have not been saved. Please post your work or save the draft if you want to keep them.': '您的更改尚未保存。如果您想保留,请发布作品或保存草稿。',
'Are you sure you want to change your username?': '您确定要更改用户名吗?',
'This has been deleted, sorry!': '抱歉,此内容已被删除!',
'Collection status updated!': '合集状态已更新!',
'The pseud was successfully deleted.': '笔名已成功删除。',
'Pseud was successfully deleted.': '笔名已成功删除。',
'You can only see your own drafts, sorry!': '抱歉!您只可以查看您自己的草稿。',
// 标签说明
'This tag indicates adult content.': '此标签涉及成人内容。',
'Parent tags (more general):': '母级标签(更通用):',
'Tags with the same meaning:': '同义标签:',
'Metatags:': '元标签:',
'Subtags:': '子标签:',
'Child tags (displaying the first 300 of each type):': '子标签(每种类型显示前 300 个):',
'and more': '以及更多',
'Relationships by Character': '关系按角色分类'
},
'innerHTML_regexp': [
['h4.heading', /^\s*Hi,\s+(.+?)!\s*$/s, '您好,$1!'],
[
'li.dropdown a.dropdown-toggle',
/^\s*Hi,\s+(.+?)!\s*$/s,
'您好,$1!'
],
// 用户主页
['li a, li span.current', /^\s*Works\s*\((\d+)\)\s*$/s, '作品($1)'],
['li a, li span.current', /^\s*Drafts\s*\((\d+)\)\s*$/s, '草稿($1)'],
['li a, li span.current', /^\s*Series\s*\((\d+)\)\s*$/s, '系列($1)'],
['li a, li span.current', /^\s*Bookmarks\s*\((\d+)\)\s*$/s, '书签($1)'],
['li a, li span.current', /^\s*Collections\s*\((\d+)\)\s*$/s, '合集($1)'],
['li a, li span.current', /^\s*Inbox\s*\((\d+)\)\s*$/s, '消息中心($1)'],
['li a, li span.current', /^\s*Sign-ups\s*\((\d+)\)\s*$/s, '报名挑战($1)'],
['li a, li span.current', /^\s*Assignments\s*\((\d+)\)\s*$/s, '任务中心($1)'],
['li a, li span.current', /^\s*Claims\s*\((\d+)\)\s*$/s, '我的认领($1)'],
['li a, li span.current', /^\s*Related Works\s*\((\d+)\)\s*$/s, '相关作品($1)'],
['li a, li span.current', /^\s*Gifts\s*\((\d+)\)\s*$/s, '接收赠文($1)'],
['li a, li span.current', /^\s*Challenge Sign-ups\s*$/s, '挑战活动报名'],
['li a, li span.current', /^\s*Gifts\s*$/s, '接收赠文'],
['a', /^\s*Unsubscribe from (.+?)\s*$/s, '取消订阅 $1'],
['h2.heading', /^\s*Works by\s+(.+?)\s*$/s, '$1 的作品'],
['h2.heading', /^\s*Series by\s+(.+?)\s*$/s, '$1 的系列'],
['h2.heading', /^\s*Bookmarks by\s+(.+?)\s*$/s, '$1 的书签'],
['h2.heading', /^\s*Collections by\s+(.+?)\s*$/s, '$1 的合集'],
['h2.heading', /^\s*Gifts for\s+(.+?)\s*$/s, '$1 收到的赠文'],
['h2.heading', /^\s*(.+?)'s Related Works\s*$/s, '$1 的相关作品'],
['h2.heading', /^\s*(.+?)'s Collections\s*$/s, '$1 的合集'],
['h2.heading', /^\s*Challenge Sign-ups for\s+(.+?)\s*$/s, '$1 参加的挑战'],
[
'h2.heading',
/^\s*(\d+)\s+Works?\s+by\s+(.+?)\s+in\s+(]+>.+?<\/a>)\s*$/s,
'$3($2):$1 篇作品'
],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Works?\s+by\s+(.+?)\s+in\s+(]+>.+?<\/a>)\s*$/s,
'$5($4):$3 篇作品,第 $1 - $2 篇'
],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Works?\s+in\s+(]+>.+?<\/a>)\s*$/s,
'$4:$3 篇作品,第 $1 - $2 篇'
],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Works?\s+by\s+(.+?)\s*$/s,
'$4:$3 篇作品,第 $1 - $2 篇'
],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Series\s+by\s+(.+?)\s*$/s,
'$4:$3 个系列,第 $1 - $2 个'
],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Bookmarks?\s+by\s+(.+?)\s*$/s,
'$4:$3 条书签,第 $1 - $2 条'
],
['h2.heading', /^\s*(\d+)\s+Works?\s+by\s+(.+?)\s*$/s, '$2:$1 篇作品'],
['h2.heading', /^\s*(\d+)\s+Series\s+by\s+(.+?)\s*$/s, '$2:$1 个系列'],
['h2.heading', /^\s*(\d+)\s+Bookmarks?\s+by\s+(.+?)\s*$/s, '$2:$1 条书签'],
['h2.heading', /^\s*(\d+)\s+Collections?\s+by\s+(.+?)\s*$/s, '$2:$1 个合集'],
['h2.heading', /^\s*(\d+)\s+Unposted\s+Drafts?\s*$/s, '未发布的草稿:$1'],
// 浏览
[
'p',
/^\s*These are some of the latest works posted to the Archive\. To find more works, choose a fandom<\/a> or try our advanced search<\/a>\.\s*(?:)?\s*$/s,
'这里展示了一些最新发布的作品。若要查看更多作品,请选择一个同人圈或尝试高级搜索。'
],
[
'p',
/^\s*These are some of the latest bookmarks created on the Archive\. To find more bookmarks,\s*choose a fandom<\/a>\s*or\s*try our advanced search<\/a>\.\s*(?:)?\s*$/s,
'这里展示了一些最新创建的书签。若要查看更多书签,请选择一个同人圈或尝试高级搜索。'
],
[
'p',
/^\s*These are some of the most popular tags used on the Archive\. To find more tags,\s*try our tag search<\/a>\.\s*$/s,
'这里展示了一些最常用的标签。若要查看更多标签,请尝试标签搜索。'
],
[
'h2.heading',
/^\s*Chapter Index for\s+(.+?<\/a>)\s+by\s+(.+?<\/a>)\s*$/s,
'章节索引:$1 by $2'
],
['p', /^\s*([\d,]+)\s+Found<\/strong>\s*$/, '找到 $1 条结果'],
['h2.heading', /^\s*([\d,]+)\s+Works?\s+in\s+(]+>.+?<\/a>)\s*$/s, '$2:$1 篇作品'],
['dd.expandable dl.range dt label', /^From$/s, '从'],
['dd.expandable dl.range dt label', /^To$/s, '到'],
['label[for*="_work_search_category_ids_"] span:last-of-type', /^(Other)(\s*\(\d+\))$/s, '其她$2'],
['label[for*="_bookmark_search_category_ids_"] span:last-of-type', /^(Other)(\s*\(\d+\))$/s, '其她$2'],
['h2.heading', /^\s*(\d+)\s*-\s*(\d+)\s*of\s*([0-9,]+)\s*Bookmarks by\s*(.+)\s*$/s, '$4:$3 条书签,第 $1 - $2 条'],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Works?\s+by\s+(.+?)\s+in\s+(]+>.+?<\/a>)\s*$/s,
'$5($4):$3 篇作品,第 $1 - $2 篇'
],
['h2.heading', /^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Works?\s+by\s+(.+)\s*$/s, '$4:$3 篇作品,第 $1 - $2 篇'],
['h2.heading', /^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+(?:Bookmarked Items|已创建书签作品) in\s+(]+>.+?<\/a>)\s*$/s, '$4:$3 篇已创建书签作品,第 $1 - $2 篇'],
['h2.heading', /^\s*Gifts for\s+(.+)\s*$/s, '$1 收到的赠文'],
['h2.heading', /^\s*(.+)'s Collections\s*$/s, '$1:合集'],
['h5.byline.heading', /^\s*Bookmarked by\s*()/s, '创建者:$1'],
['li', /^\s*Part (\d+<\/strong>) of ()/, '$2 第 $1 部分'],
['h2.heading', /^New bookmark for (.*?<\/a>)/, '为 $1 创建新书签'],
['h5.heading a', /^(\d+)\s+works?$/s, '$1 篇作品'],
['h5.heading a', /^(\d+)\s+recs?$/s, '$1 条推荐'],
['h2.heading', /^\s*Items\s+by\s+(.+?)\s+in\s+Collections\s*$/s, '$1 在合集中的作品'],
['dd a', /^([\d,]+)\s+works?$/s, '$1 篇作品'],
['h2.heading', /^\s*([\d,]+)\s+Works?\s*$/s, '$1 篇作品'],
['h2.heading', /^\s*([\d,]+)\s+Collections?\s*$/s, '$1 个合集'],
[
'dt',
/(<\/a>)\s*\(Work\)\s+by\s*(.*?<\/a>|[^<]+)/s,
'$1(作品)by $2'
],
[
'dt',
/(<\/a>)\s*\(Series\)\s+by\s*(]*>/g,
'
'
],
['li.pseud ul a[href$="/pseuds"], li.pseud ul span.current', /^\s*All Pseuds\s*\((\d+)\)\s*$/s, '所有笔名 ($1)'],
// 个人资料
['p.character_counter', /(]*>\d+<\/span>)\s*characters left/g, '剩余 $1 字符'],
['p#password-field-description', /^\s*6 to 40 characters\s*$/, '6 到 40 字符'],
['p.notice', /Any personal information you post on your public AO3 profile[\s\S]*?(?:Privacy Policy|隐私政策)<\/a>[\s\S]*?\./s, '您在公开 AO3 个人资料中发布的任何个人信息(包括但不限于您的姓名、电子邮箱、年龄、位置、个人关系、性别或性取向认同、种族或族裔背景、宗教或政治观点,以及/或其她网站的账户用户名)都会对公众可见。要了解 AO3 在您使用网站时收集哪些数据以及我们如何使用这些数据,请查看我们的隐私政策。'],
['div.caution.notice', /\s*Please use this feature with caution\.<\/strong>[\s\S]*?<\/p>/s, '请谨慎使用此功能。用户名每 7 天仅能更改一次。
'],
['div.notice', /Changing your email will send a request for confirmation[\s\S]*?will invalidate any pending email change requests<\/strong>\./s, '更改电子邮箱将向您的新邮箱发送确认请求,并向当前邮箱发送通知。
您必须使用确认邮件中的链接完成邮箱更改。如在 7 天内未确认,请求链接将失效,邮箱不会更改。
重新提交新邮箱请求将使任何未完成的更改请求失效。'],
['p.footnote', /You cannot change the pseud that matches your username\. However, you can change your username<\/a> instead\./g, '无法修改与用户名相同的笔名。如需修改,请更改您的用户名。'],
['h2.heading', /^Pseuds for (.+)$/, '$1 的笔名'],
['div.caution.notice p:last-child', /For information on how changing your username will affect your account.*?contact Support.*?\./s, '要了解更改用户名对账户的影响,请参阅账户常见问题。用户名变更可能需要数天或更长时间才会生效。如果一周后您的作品、书签、系列或合集中仍显示旧用户名,请联系支持团队。'],
['p.note', /If that is not what you want.*?create a new Pseud.*?instead\./s, '如果您不想更改用户名,也可以创建一个新的笔名。'],
['p.footnote', /3 to 40 characters.*?underscore.*?\)/s, '3 至 40 个字符(仅限 A–Z、a–z、_、0–9),禁止使用空格,且不能以下划线开头或结尾'],
[
'div.caution.notice p',
/For information on how changing your username will affect your account[\s\S]*?contact Support<\/a>[.。]?/s,
'有关更改用户名如何影响账户的详情,请参阅账户常见问题。用户名更改可能需要数天或更长时间才会生效。如果一周后您的作品、书签、系列或合集中仍显示旧用户名,请联系支持团队。'
],
[
'div.flash.notice',
/^\s*Your password has been changed\. To protect your account, you have been logged out of all active sessions\. Please log in with your new password\.\s*$/s,
'您的密码已更改。为了保护您的账户,您已从所有活动会话中登出。请使用新密码登录。'
],
// 作品
[
'span.role',
/^\s*\(Guest\)\s*$/i,
' (访客)'
],
[
'span.parent',
/^\s*on\s+(]+>)Chapter\s+(\d+)(<\/a>)\s*$/is,
':$1第 $2 章$3'
],
[
'span.parent',
/^\s*on\s+(]+>)Chapter\s+(\d+)\s+of\s+(\d+)(<\/a>)\s*$/is,
':$1第 $2 章 / 共 $3 章$4'
],
[
'h2.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Series\s+by\s+(.+?)\s*$/s,
'$4:$3 个系列,第 $1 - $2 个'
],
[
'h3.heading',
/^\s*(\d+)\s*-\s*(\d+)\s+of\s+([0-9,]+)\s+Collections\s*$/s,
'$3 个合集,第 $1 - $2 个'
],
[
'p.type',
/^\s*\((Open|Closed)(.*)\)\s*$/s,
(_match, status, rest) => {
const map = { 'Open': '开放中', 'Closed': '已截止' };
return `(${map[status]}${rest})`;
}
],
[
'h2.heading',
/^\s*Collections including\s+(.+?)\s*$/s,
'包含 $1 的合集'
],
[
'h3.heading',
/^\s*(\d+)\s+Collections?\s*$/s,
'$1 个合集'
],
// 书签
[
'h4.heading',
/^\s*