Last active
July 22, 2025 01:54
-
-
Save flipeador/56ca9b5798987d175f0a5e9bc97f6977 to your computer and use it in GitHub Desktop.
Translate Bluesky posts using the Translator API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Bluesky Translate | |
// @author Flipeador | |
// @version 1.0.1 | |
// @icon https://www.google.com/s2/favicons?sz=128&domain=bsky.app | |
// @homepageURL https://gist.github.com/flipeador/56ca9b5798987d175f0a5e9bc97f6977 | |
// @downloadURL https://gist.github.com/flipeador/56ca9b5798987d175f0a5e9bc97f6977/raw/bluesky-translate.js | |
// @match https://bsky.app/* | |
// @run-at document-idle | |
// ==/UserScript== | |
// https://developer.chrome.com/docs/ai/built-in | |
const gtUrl = 'https://translate.google.com/'; | |
const selector = `a[href^="${gtUrl}"]:not([_])`; | |
const detector = LanguageDetector.create(); | |
const [hrLangTagTo] = displayNames([navigator.language]); | |
async function main() { | |
for (const $a of document.querySelectorAll(selector)) { | |
$a.setAttribute('_', ''); | |
let text = new URL($a.href).searchParams.get('text'); | |
const [result] = await (await detector).detect(text); | |
if (result.detectedLanguage !== 'und' && result.confidence >= 0.1) { | |
const [hrLangTagFrom] = displayNames([result.detectedLanguage]); | |
$a.setAttribute('title', `${hrLangTagFrom} → ${hrLangTagTo}`); | |
async function onTranslate(event) { | |
event.preventDefault(); | |
event.stopImmediatePropagation(); | |
const $p = await translate(text, result.detectedLanguage); | |
$a.parentElement.insertAdjacentElement('afterend', $p); | |
} | |
$a.addEventListener('click', onTranslate, { once: true }); | |
} | |
} | |
setTimeout(main, 2500); | |
} | |
async function translate(text, from, to) { | |
const element = document.createElement('p'); | |
function onProgress(event) { | |
const progress = (event.loaded * 100).toFixed(2); | |
element.textContent = `Downloaded: ${progress}%`; | |
} | |
const availability = await findAvailableLanguage(from, to); | |
const sourceLanguage = from, { targetLanguage } = availability; | |
const monitor = m => m.addEventListener('downloadprogress', onProgress); | |
const p = Translator.create({ monitor, sourceLanguage, targetLanguage }); | |
const promise = p.then(translator => translator.translateStreaming(text)); | |
promise.then(async stream => { | |
element.textContent = ''; | |
for await (const chunk of stream) | |
element.textContent += chunk; | |
}); | |
return element; | |
} | |
async function findAvailableLanguage(sourceLanguage, targetLanguages) { | |
targetLanguages ??= navigator.languages; | |
for (const targetLanguage of targetLanguages) { | |
const options = { sourceLanguage, targetLanguage }; | |
options.availability = await Translator.availability(options); | |
if (options.availability !== 'unavailable') return options; | |
} | |
} | |
function displayNames(codes, options={ type: 'language' }) { | |
const displayNames = new Intl.DisplayNames([navigator.language], options); | |
return codes.map(code => displayNames.of(code)); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Bluesky Translate
Translate Bluesky posts using the Translator API, available from Chrome 138 stable.
Note
The
Translate
link restores its default behavior after the first click.Installation