Amazon International Links

Add international links to Amazon product pages

// ==UserScript==
// @name          Amazon International Links
// @description   Add international links to Amazon product pages
// @author        chocolateboy
// @copyright     chocolateboy
// @version       3.7.0
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL
// @include       https://smile.amazon.tld/*
// @include       https://www.amazon.com.be/*
// @include       https://www.amazon.tld/*
// @require       https://code.jquery.com/jquery-3.6.1.slim.min.js
// @require       https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@43fd0fe4de1166f343883511e53546e87840aeaf/gm_config.js
// @require       https://cdn.jsdelivr.net/gh/aduth/hijinks@b4fbbd462c98248c585659fcc35a50b00fec147c/hijinks.min.js
// @grant         GM_registerMenuCommand
// @grant         GM_getValue
// @grant         GM_setValue
// ==/UserScript==

// XXX GM_getValue and GM_setValue are used by GM_config

/*
 *
 * further reading:
 *
 *     https://helpful.knobs-dials.com/index.php/Amazon_notes#Links
 */

/*********************** Constants ********************************/

/*
 * a map from the Amazon TLD to the corresponding two-letter country code
 *
 * XXX technically, UK should be GB: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
 */
const SITES = {
    'com.au': 'AU', // Australia
    'com.be': 'BE', // Belgium
    'com.br': 'BR', // Brazil
    'ca':     'CA', // Canada
    'cn':     'CN', // China
    'fr':     'FR', // France
    'de':     'DE', // Germany
    'in':     'IN', // India
    'it':     'IT', // Italy
    'co.jp':  'JP', // Japan
    'com.mx': 'MX', // Mexico
    'nl':     'NL', // Netherlands
    'es':     'ES', // Spain
    'se':     'SE', // Sweden
    'com.tr': 'TR', // Turkey
    'ae':     'AE', // UAE
    'co.uk':  'UK', // UK
    'com':    'US', // US
}

/*
 * Amazon TLDs which support the "smile.amazon" subdomain
 */
const SMILE = new Set(['com', 'co.uk', 'de'])

/*
 * a tiny DOM builder to avoid cluttering the code with HTML templates
 * https://github.com/aduth/hijinks
 */
const el = hijinks

/*********************** Functions and Classes ********************************/

/*
 * A class which encapsulates the logic for creating and updating cross-site links
 */
class Linker {
    /*
     * get the unique identifier (ASIN - Amazon Standard Identification Number)
     * for this product, or return a falsey value if it's not found
     */
    static getASIN () {
        let asin, $asin = $('input#ASIN, input[name="ASIN"], input[name="ASIN.0"]')

        if ($asin.length) {
            asin = $asin.val()
        } else { // if there's a canonical link, try to retrieve the ASIN from its URI
            // <link rel="canonical" href="https://www.amazon.com/Follows-Movie-Poster-18-28/dp/B01BKUBARA" />
            let match, canonical = $('link[rel="canonical"][href]').attr('href')

            if (canonical && (match = canonical.match('/dp/(\\w+)$'))) {
                asin = match[1]
            }
        }

        return asin
    }

    constructor (asin) {
        // the unique Amazon identifier for this product
        this.asin = asin

        // the navbar to add the cross-site links to
        this.crossSiteLinks = $('#nav-xshop')

        // an array of our added elements - jQuery objects representing
        // <a>...</a> links
        //
        // we keep a reference to these elements so we can easily remove them
        // from the DOM (and replace them with new elements) whenever the
        // country selection changes
        this.links = []

        // extract and store 1) the subdomain (e.g. "www.amazon") and 2) the TLD
        // (e.g. "co.uk") of the current site
        const parts = location.hostname.split('.')

        // 1) the subdomain (part before the TLD) of the current site e.g.
        // "www.amazon" or "smile.amazon"
        this.subdomain = parts.slice(0, 2).join('.')

        // 2) the TLD of the current site e.g. "co.uk" or "com"
        this.tld = parts.slice(2).join('.')
    }

    /*
     * add a link element to the internal `links` array
     */
    addLink (tld, country) {
        const attrs = {
            class: 'nav-a',
            style: 'display: inline-block',
            title: `amazon.${tld}`
        }

        // XXX we can't always preserve the "smile.amazon" subdomain as it's not
        // available for most Amazon TLDs
        const subdomain = SMILE.has(tld) ? this.subdomain : 'www.amazon'

        let tag

        if (tld === this.tld) {
            tag = 'strong'
        } else {
            tag = 'a'
            attrs.href = `//${subdomain}.${tld}/dp/${this.asin}`
        }

        const link = el(tag, attrs, country)

        this.links.push($(link))
    }

    /*
     * populate the array of links and display them by prepending them to the
     * body of the cross-site navigation bar
     */
    addLinks () {
        // create the subset of the TLD -> country-code map (SITES)
        // corresponding to the enabled sites
        const sites = Object.entries(SITES)
            .filter(([tld]) => GM_config.get(tld))
            .reduce((obj, [key, val]) => { return obj[key] = val, obj }, {})

        if (!$.isEmptyObject(sites)) {
            // sort the sites by the country code (e.g. AU) rather than the TLD
            // (e.g. com.au)
            // const tlds = sortBy(Object.keys(sites), tld => sites[tld])
            const tlds = Object.keys(sites).sort((a, b) => sites[a].localeCompare(sites[b]))

            // populate the `links` array with jQuery wrappers for link elements
            // (i.e. <a>...</a>)
            for (const tld of tlds) {
                this.addLink(tld, sites[tld])
            }

            // prepend the cross-site links to the body of the crossSiteLinks
            // container
            this.crossSiteLinks.prepend.apply(this.crossSiteLinks, this.links)
        }
    }

    /*
     * build the underlying data model used by the GM_config utility
     */
    initializeConfig () {
        const checkboxes = {}

        // sort by country code
        for (const tld of Object.keys(SITES).sort((a, b) => SITES[a].localeCompare(SITES[b]))) {
            const country = SITES[tld]

            checkboxes[tld] = {
                type: 'checkbox',
                label: country,
                title: `amazon.${tld}`,
                default: (country === 'UK' || country === 'US')
            }
        }

        // re-render the links when the settings are updated
        const save = () => {
            this.removeLinks()
            this.addLinks()
            GM_config.close()
        }

        const callbacks = { save }

        GM_config.init('Amazon International Links Settings', checkboxes, callbacks)
    }

    /*
     * remove all added links from the DOM and clear the array referencing them
     */
    removeLinks () {
        const { links } = this

        for (const $link of links) {
            $link.remove() // remove from the DOM...
        }

        links.length = 0 // ...and empty the array
    }
}

/*********************** Main ********************************/

const run = () => {
    const asin = Linker.getASIN()

    if (asin) {
        const showConfig = () => GM_config.open() // display the settings manager
        const linker = new Linker(asin)

        linker.initializeConfig()
        linker.addLinks()

        GM_registerMenuCommand('Configure Amazon International Links', showConfig)
    }
}

run()