AO3: just my languages

Reduce language options to your preferences

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AO3: just my languages
// @namespace    https://greasyfork.org/en/users/757649-certifieddiplodocus
// @version      1.2.3
// @description  Reduce language options to your preferences
// @author       CertifiedDiplodocus
// @match        http*://archiveofourown.org/*
// @exclude      /^https?:\/\/archiveofourown\.org(?!\/search$|(.*\/(works|bookmarks)(?![^\/?])))/
// @exclude      /.org/(works|bookmarks)$/
// @exclude      /(works|bookmarks)\/search[?](?!.*edit_search=true)/
// @exclude      /\/works\/[0-9]+(?![0-9]*\/edit)/
// @exclude      /\/bookmarks\/[0-9]+
// @icon         data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>𒈾</text></svg>
// @grant        GM_addStyle
// @license      GPL-3.0-or-later
// ==/UserScript==

/* PURPOSE: Simplify language search options on AO3. Choose any combination of the following:
    1 - Show only your chosen languages in the dropdown list (when filtering and/or creating or editing a work)
    2 - Bold some languages in the dropdown (can be chosen independently from option 1)
    3 - Autofill:
            - Monolingual: automatically set the dropdown to your preferred language
            - Multilingual: add a query to each search to show fic in multiple languages at once (e.g. English AND Spanish AND Thai)
    4 - Manual multilingual search: click the 𒈾 button to add/remove multilingual filters. Also works with autofill.

When creating, importing or editing a work, you can:
    5 - Show only your chosen languages in the dropdown
    6 - Set a default language (⚠ use with caution ⚠)

TIP: add all languages close to yours (e.g. if you read English you can read Scots, if you read Spanish
you have a fair shot at Galego and Asturianu, etc)
---------------------------------------------------------------------------------------------------------------------- */
(function () {
    'use strict'
    const $ = document.querySelector.bind(document) // shorthand for readability
    const $$ = document.querySelectorAll.bind(document)

    /* ======================== 𒈾 USER SETTINGS (save to plaintext file in case of script updates) 𒈾 ==============================

    LANGUAGE CODES are listed at the end of this script. (AO3 appears to use a mix of 2 and 3-character codes.)
    Leave blank for the AO3 default appearance.

    OPTIONAL: autofill new searches to your chosen language(s). Filters can still be changed by hand.
    This carries a small risk of hiding mistagged fics, so is disabled by default (filtering.autofill: 0).
        0 - AO3 default (blank dropdown)
        1 - MONOLINGUAL autofill (fills the dropdown with your preferred language)
        2 - MULTILINGUAL autofill (adds a search query to show fic in all your preferred languages at once)
               * note: may be slower / have more impact on the servers. If noticeable, try reducing the number of languages. */

    const languages = {
        dropdown: ['en', 'es', 'fr', 'ptBR', 'ptPT', 'sux'],
        bolded: ['en', 'es', 'fr'],
        multilingualSearch: ['ptBR', 'ptPT', 'sux'],
    }
    const filtering = {
        modifyDropdown: true,
        autofill: 0,
        defaultLanguage: 'es',
    }
    const editing = {
        modifyDropdown: true,
        defaultLanguage: 'en', // OPTIONAL: add a language to autofill on new works
    }

    // ===============================================================================================================================

    // Check that settings make sense
    const errPrefix = '[AO3: just my languages - userscript] \n⚠ Error: '
    if (!languages.dropdown.some(Boolean) && (filtering.modifyDropdown || editing.modifyDropdown)) {
        throw errPrefix + 'To modify the dropdown you must add some languages first!'
    }
    if (filtering.autofill === 1 && !filtering.defaultLanguage) { throw errPrefix + 'To autofill the dropdown, add a default language first!' }
    if (filtering.autofill === 2 && !(languages.multilingualSearch || languages.dropdown)) { throw errPrefix + 'To autofill a multilingual search, add some languages first!' }

    const pageURL = window.location.href
    const changeEvent = new Event('change')
    let dropdown, searchbox

    // CSS
    GM_addStyle (`
    .babel-button {
        cursor: copy;
    }
    span.babel-normal-align {
        vertical-align: inherit;
    }
    span.babel-button-filter-on {
        color: mintcream;
        background-color: darkgreen;
        border-color: darkgreen;
    }
    .just-my-langs > option {
        display: none;
        &[value=''], /* always include the default option (blank) */
        &.jml__show {
            display: initial;
        }
        &.jml__bold {
            font-weight: bold;
        }
    }`)

    // -------------------------------------------------------------------------------------------------------------------------------

    // Show only selected languages when creating/editing works
    if ((/\/works\/(new.*|([0-9]+\/edit))/gi).test(pageURL)) {
        const dropdowns = $$('select[id$="language_id"') // handle language selection in new?imported page, including parent work (IDs are different but all end in "language_id")
        for (dropdown of dropdowns) {
            verifyLanguageCodes()
            if (editing.modifyDropdown) { reduceDropdownLangs(); boldDropdownLangs() }
            if (pageURL.includes('/works/new')) { autofillBlankDropdown(editing.defaultLanguage) }
        }
        return
    }

    // -------------------------------------------------------------------------------------------------------------------------------

    // select the right elements for the page
    if (pageURL.includes('/bookmarks')) {
        dropdown = $('#bookmark_search_language_id')
        searchbox = $('#bookmark_search_bookmarkable_query')
    } else {
        dropdown = $('#work_search_language_id')
        searchbox = $('#work_search_query')
    }
    verifyLanguageCodes()

    // show only my languages (and the default 'blank' value) in the dropdown
    if (filtering.modifyDropdown) { reduceDropdownLangs(); boldDropdownLangs() }

    // Set filter for searching multiple languages. (If user didn't fill in multiLanguages, use languages.dropdown instead.)
    const languageFilters = 'language_id:' + (languages.multilingualSearch || languages.dropdown).join(' OR language_id:')

    /* Autofill (if the dropdown/searchbox are blank)
        1 - MONOLINGUAL AUTOFILL: set dropdown to the default language.
        2 - MULTILINGUAL AUTOFILL: insert search string into "Search within results / Any field": "language_id:egy OR language_id:sux"   */

    switch (filtering.autofill) {
        case 1:
            autofillBlankDropdown(filtering.defaultLanguage)
            break
        case 2:
            if (searchbox.value.trim().length === 0) { searchbox.value = languageFilters }
    }

    // Add (𒈾) button for multilingual searches next to "Languages" label.
    const dropdownLabel = dropdown.parentElement.previousElementSibling
    const babelButton = createNewElement('a', 'question')
    const span = createNewElement('span', 'symbol question babel-button', '𒈾')
    babelButton.append(span)
    dropdownLabel.append(babelButton)

    // On click of (𒈾), add OR remove language filters from the "all fields" searchbox (after the current query)
    babelButton.addEventListener('click', function (e) {
        const searchboxContent = searchbox.value.trim()
        if (searchboxContent.length === 0) {
            searchbox.value = languageFilters
        } else if (!searchboxContent.includes(languageFilters)) {
            searchbox.value = searchboxContent + ' ' + languageFilters // toggle on
        } else {
            searchbox.value = searchboxContent.replace(languageFilters, '').trim() // toggle off
        }
        searchbox.dispatchEvent(changeEvent)
    })

    // Conditional CSS: alignment + colour (on search pages, 𒈾 should use the default page style to match the neighbouring "?")
    span.classList.toggle('babel-normal-align', !pageURL.includes('/search'))
    searchbox.addEventListener('change', indicateBabelStatus)
    indicateBabelStatus()

    // -------- FUNCTIONS ------------------------------------------------------------------------------------------------------------

    // Colour 𒈾 green as long as the search box contains the filter (check with different site skins). Set the tooltip.
    function indicateBabelStatus() {
        const languageFiltersOn = searchbox.value.includes(languageFilters)
        span.classList.toggle('babel-button-filter-on', languageFiltersOn)
        babelButton.setAttribute(
            'title', `${languageFiltersOn ? 'Searching' : 'Search'} multiple languages:\n${languages.multilingualSearch.join(', ')}`
        )
    }

    // Show only your chosen languages (+ blank option) in the dropdown (filter or editing)
    function reduceDropdownLangs() {
        dropdown.classList.add('just-my-langs')
        for (const userLang of languages.dropdown) {
            dropdown.querySelector(`[lang="${userLang}"]`).classList.add('jml__show')
        }
    }

    // Bold languages in the dropdown
    function boldDropdownLangs() {
        if (!languages.bolded.some(Boolean)) { return }
        for (let userLang of languages.bolded) {
            dropdown.querySelector(`[lang="${userLang}"]`).classList.add('jml__bold')
        }
    }

    // Autofill the dropdown if it is empty and the user selected a default language
    function autofillBlankDropdown(defaultLang) {
        if (!defaultLang || dropdown.value) { return }
        dropdown.querySelector(`[lang="${defaultLang}"]`).setAttribute('selected', 'selected')
    }

    // Check that all user languages exist (run after the dropdown is set)
    function verifyLanguageCodes() {
        if (!dropdown.length) { throw errPrefix + 'No dropdown found!' }
        const allUserLanguages = new Set( // no duplicates
            [...languages.dropdown, ...languages.bolded, ...languages.multilingualSearch, 
                filtering.defaultLanguage, editing.defaultLanguage]
                .filter(x => x) // no empty values
        )
        const ao3LangList = new Set(
            [...dropdown.children].map(el => el.getAttribute('lang'))
        )
        const invalidLanguageCodes = allUserLanguages.difference(ao3LangList)
        if (invalidLanguageCodes.size === 0) { return true }
        console.error(errPrefix + 'Could not find these language codes: "' + [...invalidLanguageCodes].join(', ') + '"\n'
            + 'Please check your settings for typos.\n\n'
            + 'User-selected languages: ' + [...allUserLanguages].join(', '))
        return false
    }

    function createNewElement(elementType, className, textContent) {
        const el = document.createElement(elementType)
        el.className = className
        el.textContent = textContent
        return el
    }

    /* --------- LANGUAGE CODES ON AO3 ------------------------------------------------------------------------------------------------

so:  af Soomaali
afr: Afrikaans
ain: Aynu itak | アイヌ イタㇰ
ar:  العربية
amh: አማርኛ
egy: 𓂋𓏺𓈖 𓆎𓅓𓏏𓊖
arc: ܐܪܡܝܐ | ארמיא
hy:  հայերեն
ase: American Sign Language
ast: asturianu
id:  Bahasa Indonesia
ms:  Bahasa Malaysia
bg:  Български
bn:  বাংলা
jv:  Basa Jawa
ba:  Башҡорт теле
be:  беларуская
bos: Bosanski
br:  Brezhoneg
ca:  Català
ceb: Cebuano
cs:  Čeština
chn: Chinuk Wawa
crh: къырымтатар тили | qırımtatar tili
cy:  Cymraeg
da:  Dansk
de:  Deutsch
et:  eesti keel
el:  Ελληνικά
sux: 𒅴𒂠
en:  English
ang: Eald Englisċ
es:  Español
eo:  Esperanto
eu:  Euskara
fa:  فارسی
fil: Filipino
fr:  Français
frr: Friisk
fur: Furlan
ga:  Gaeilge
gd:  Gàidhlig
gl:  Galego
got: 𐌲𐌿𐍄𐌹𐍃𐌺𐌰
gyn: Creolese
hak: 中文-客家话
ko:  한국어
hau: Hausa | هَرْشَن هَوْسَ
hi:  हिन्दी
hr:  Hrvatski
haw: ʻŌlelo Hawaiʻi
ia:  Interlingua
zu:  isiZulu
is:  Íslenska
it:  Italiano
he:  עברית
kal: Kalaallisut
kan: ಕನ್ನಡ
kat: ქართული
cor: Kernewek
khm: ភាសាខ្មែរ
qkz: Khuzdul
sw:  Kiswahili
ht:  kreyòl ayisyen
ku:  Kurdî | کوردی
kir: Кыргызча
fcs: Langue des signes québécoise
lv:  Latviešu valoda
lb:  Lëtzebuergesch
lt:  Lietuvių kalba
la:  Lingua latina
hu:  Magyar
mk:  македонски
ml:  മലയാളം
mt:  Malti
mnc: ᠮᠠᠨᠵᡠ ᡤᡳᠰᡠᠨ
qmd: Mando&#39;a
mr:  मराठी
mik: Mikisúkî
mon: ᠮᠣᠩᠭᠣᠯ ᠪᠢᠴᠢᠭ᠌ | Монгол Кирилл үсэг
my:  မြန်မာဘာသာ
myv: Эрзянь кель
nah: Nāhuatl
nan: 中文-闽南话 臺語
ppl: Nawat
nl:  Nederlands
ja:  日本語
no:  Norsk
azj: Азәрбајҹан дили | آذربایجان دیلی
ce:  Нохчийн мотт
ood: ‘O’odham Ñiok
ota: لسان عثمانى
ps:  پښتو
nds: Plattdüütsch
pl:  Polski
ptBR:  Português brasileiro
ptPT:  Português europeu
pa:  ਪੰਜਾਬੀ
kaz: qazaqşa | қазақша
qlq: Uncategorized Constructed Languages
qya: Quenya
ro:  Română
ru:  Русский
sco: Scots
sq:  Shqip
sjn: Sindarin
si:  සිංහල
sk:  Slovenčina
slv: Slovenščina
gem: Sprēkō Þiudiskō
sr:  Српски
fi:  suomi
sv:  Svenska
ta:  தமிழ்
tat: татар теле
mri: te reo Māori
tel: తెలుగు
th:  ไทย
tqx: Thermian
bod: བོད་སྐད་
vi:  Tiếng Việt
cop: ϯⲙⲉⲧⲣⲉⲙⲛ̀ⲭⲏⲙⲓ
tlh: tlhIngan-Hol
tok: toki pona
trf: Trinidadian Creole
tsd: τσακώνικα
chr: ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ
tr:  Türkçe
uk:  Українська
urd: اُردُو
uig: ئۇيغۇر تىلى
vol: Volapük
wuu: 中文-吴语
yi:  יידיש
yua: maayaʼ tʼàan
yue: 中文-广东话 粵語
zh:  中文-普通话 國語


Userscript should activate only on URLs with language filtering.
If it doesn't activate on a page, and it should, let me know.
(Quick & dirty fix: delete the @exclude lines at the start of the script to enable the script on all of AO3.)

Include: all URLS with /works or /bookmarks; new/edit work; the exact url "https://archiveofourown.org/search"
Exclude: individual works/bookmarks, advanced search results (no tag filtering = no filter sidebar)

INCLUDE EXAMPLES
users/singlecrow/pseuds/raven/works?fandom_id=47992099
collections/SomeCollection/bookmarks
collections/SomeCollection/works?work_search%5...
languages/uig/works
bookmarks?bookmark_search%5Bsort_column%5D=cre...
bookmarks/search
works?work_search
works/new
works/new?import=true
works/123456/edit
works/123456/edit_tags
(works|bookmarks)/search?.*edit_search=true
search?commit=Search&edit_search=true

EXCLUDE EXAMPLES
works/search?commit=Search&work_search%5Bquery...       (no filter bar)
works/search?work_search%5Bquery%5D=&work_search...
works/search.*(?!edit_search=true)
works/123456
bookmarks/123456/edit
collections/Suggested_Good_Reads/works/18356108
.org/works
.org/bookmarks

TAMPERMONKEY REGEX ISSUE: the line
@exclude      /\/works\/[0-9]+(?![0-9]*\/edit)/
should also block /works/123/comments/edit, but doesn't. See https://stackoverflow.com/questions/68826178/exclude-in-userscript-not-working-as-expected
for another script where the regex fails in Tampermonkey, but not Violentmonkey
*/

})()