Last active
November 8, 2022 19:35
LinkedIn Learning ترجمة دورات لينكدإن ليرننج إلى اللغة العربية آليا
This file contains 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
// طريقة التشغيل من موقعي على الرابط التالي: blog.abdelhadi.org | |
// ==UserScript== | |
// @name LinkedIn Learning ترجمة لينكدإن ليرننج | |
// @description LinkedIn Learning ترجمة دورات لينكدإن ليرننج إلى اللغة العربية | |
// @namespace https://github.com/journey-ad | |
// @version 0.2.1 | |
// @icon https://static.licdn.cn/sc/h/2c0s1jfqrqv9hg4v0a7zm89oa | |
// @author journey-ad | |
// @match *://www.linkedin.com/learning/* | |
// @require https://greasyfork.org/scripts/411512-gm-createmenu/code/GM_createMenu.js?version=864854 | |
// @require https://cdn.jsdelivr.net/npm/fingerprintjs2@2.1.0/fingerprint2.min.js | |
// @license MIT | |
// @run-at document-end | |
// @grant GM_registerMenuCommand | |
// @grant GM_unregisterMenuCommand | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @grant GM_xmlhttpRequest | |
// ==/UserScript== | |
const __SCRIPT_NAME = 'LinkedIn Learning ترجمة لينكد إن ليرننج ' | |
const __SCRIPT_VER = '0.2.1' | |
const logcat = { | |
log: createDebugMethod('log'), | |
info: createDebugMethod('info'), | |
debug: createDebugMethod('debug'), | |
warn: createDebugMethod('warn'), | |
error: createDebugMethod('error') | |
} | |
function createDebugMethod(name) { | |
const bgColorMap = { | |
debug: '#0070BB', | |
info: '#009966', | |
warn: '#BBBB23', | |
error: '#bc0004' | |
} | |
name = bgColorMap[name] ? name : 'info' | |
return function() { | |
const args = Array.from(arguments) | |
args.unshift(`color: white; background-color: ${bgColorMap[name] || '#FFFFFF'};`) | |
args.unshift(`【${__SCRIPT_NAME} v${__SCRIPT_VER}】 %c[${name.toUpperCase()}]:`) | |
console[name].apply(console, args) | |
} | |
} | |
(function() { | |
'use strict' | |
logcat.info('loaded') | |
let transPlat = 'caiyun' // caiyun | google | |
let sourceSub = null // مصفوفة الترجمة الأصلية | |
let __PLAYER_ = null // المشغّل | |
let __CUES = null // كائن الترجمة الأصلي | |
let ts = Date.now() | |
let transTimer = window.setInterval(init, 100) // استخدام "المؤقّت" للتحقق من أن المشغّل قد تم تحميله بنجاح | |
addCustomStyle() | |
addMenu() | |
// hook history.pushState مراقبة تغيرات المسار | |
!(function(history) { | |
const pushState = history.pushState; | |
history.pushState = function(state) { | |
logcat.debug('تغييرات توجيه الصفحة') | |
// إعادة تهيئة المسار | |
ts = Date.now() | |
window.clearInterval(transTimer) | |
transTimer = window.setInterval(init, 100) | |
return pushState.apply(history, arguments); | |
}; | |
})(window.history) | |
function init() { | |
if (Date.now() - ts >= 20 * 1000) { | |
window.clearInterval(transTimer) // مسح عدّاد المؤقت | |
logcat.error('مهلة اكتشاف المشغّل، قد لا تكون الصفحة الحالية قيد التشغيل') | |
} | |
const coursePage = document.querySelector('.classroom-body') | |
const quiz = document.querySelector('.classroom-quiz') | |
// التعامل مع حالة اختبارات قسم الدورة التدريبية | |
if (coursePage && quiz) { | |
coursePage.hasBeenInject = false | |
} | |
__PLAYER_ = document.querySelector('.media-player__player')?.player | |
const __textTracks_ = __PLAYER_?.textTracks_ | |
if (__textTracks_) { | |
window.clearInterval(transTimer) | |
logcat.debug('تم تحميل المشغّل') | |
handleHookPlayer() | |
} else { | |
return | |
} | |
function handleHookPlayer() { | |
if (coursePage.hasBeenInject) return | |
coursePage.hasBeenInject = true // ضع علامة على الصفحة الحالية أنه تم حقنها برمجياً | |
// بعد إضافة الترجمة | |
__textTracks_.on('addtrackcomplete', () => { | |
handleHookWebtt() | |
}) | |
} | |
function handleHookWebtt() { | |
// زر تبديل الترجمة | |
const { | |
captionsMenuToggle | |
} = __PLAYER_.controlBar | |
const isEnable = captionsMenuToggle.items[0].isSelected_ | |
if (!isEnable) { | |
logcat.warn('مبدّل الترجمة لم يتم تشغيله') | |
// captionsToggle.one('activate', () => { | |
// logcat.info('قم بتشغيل مبدّل الترجمة') | |
//handleHookWebtt() | |
// }) | |
return | |
} | |
let zhTrackItem = __PLAYER_.tech_.remoteTextTracks().tracks_.find(_ => _.language === 'ar-sa-addon') | |
// الإدخال الأولي لقائمة لغة الترجمة | |
if (!zhTrackItem) { | |
logcat.debug('تحميل مسار الترجمة') | |
// إضافة مسار الترجمة العربية | |
const zhTrack = __PLAYER_.tech_.createRemoteTextTrack({ | |
kind: 'captions', | |
label: 'العربية (ترجمة آلية)', | |
srclang: 'ar-sa-addon' | |
}) | |
__PLAYER_.tech_.remoteTextTrackEls().addTrackElement_(zhTrack) | |
__PLAYER_.tech_.remoteTextTracks().addTrack(zhTrack.track) | |
} | |
const tracks = __PLAYER_.tech_.remoteTextTracks() | |
zhTrackItem = tracks.tracks_.find(_ => _.language === 'ar-sa-addon') | |
// استنساخ نسخة من مسار ترجمة باللغة الإنجليزية كمسار ترجمة عربي | |
zhTrackItem.cues_ = clone(tracks.tracks_[0].cues_) | |
zhTrackItem.cues.setCues_(zhTrackItem.cues_) | |
__CUES = zhTrackItem.cues_ | |
if (__CUES.length === 0) { | |
// مسار الترجمة فارغ، حاول مرة أخرى بعد 100 مللي ثانية | |
setTimeout(handleHookWebtt, 100) | |
return | |
} | |
// منع الترجمة المتكررة | |
if (zhTrackItem.hasBeenTranslate) return | |
zhTrackItem.hasBeenTranslate = true | |
// إيقاف التشغيل مؤقتا في انتظار الترجمة عند تشغيلها | |
if (isEnable) __PLAYER_.pause() | |
// استرجع صفيف التسمية التوضيحية الأصلي | |
sourceSub = __CUES.map(_ => _.text) | |
// قم بإجراء عمليات الترجمة | |
__CUES[0].text = `[等待翻译文本]\n${__CUES[0].text}` | |
// إعادة تعيين حالة عرض الترجمة | |
captionsMenuToggle.items[0].el_.click() | |
captionsMenuToggle.items.find(_ => _.track.language === 'ar-sa-addon').el_.click() | |
transText((text) => { | |
logcat.info('字幕翻译完毕') | |
console.groupCollapsed('中文字幕文本') | |
console.info(text) | |
console.groupEnd() | |
__CUES[0].text = __CUES[0].text.replace(/\[等待翻译文本\]\n/, '') | |
// if (captionsMenuToggle.items[0].isSelected_) { | |
// استئناف التشغيل | |
if (isEnable) __PLAYER_.play() | |
// إعادة تعيين حالة عرض الترجمة | |
captionsMenuToggle.items[0].el_.click() | |
captionsMenuToggle.items.find(_ => _.track.language === 'ar-sa-addon').el_.click() | |
// } | |
}) | |
} | |
} | |
// أضف بعض الأنماط المخصصة | |
function addCustomStyle() { | |
const css = ` | |
.classroom-layout__stage--hide-controls .vjs-text-track-display { | |
bottom: 18px !important; | |
} | |
.vjs-text-track-display>div>div { | |
font-size: 1.6rem !important; | |
font-size: clamp(1.4rem, 2.2vmin, 2.4rem) !important; | |
line-height: 1.4 !important; | |
white-space: pre-wrap !important; | |
padding: 6px 20px !important; | |
direction: rtl; | |
} | |
.vjs-text-track-display>div>div>div { | |
max-width: 66ch !important; | |
} | |
` | |
addStyle(css) | |
} | |
function addMenu() { | |
GM_createMenu.add({ | |
on: { | |
default: true, | |
name: "انقر لتبديل مصدر الترجمة (حاليا: Caiyun)", | |
callback: function() { | |
transPlat = 'google' | |
alert("تم تحويل مصدر الترجمة إلى Google") | |
} | |
}, | |
off: { | |
name: "انقر للتبديل بين مصادر الترجمة (حاليا: Google)", | |
callback: function() { | |
transPlat = 'caiyun' | |
alert("تم تحويل مصدر الترجمة إلى caiyun.") | |
} | |
} | |
}); | |
GM_createMenu.create({ | |
storage: true | |
}); | |
transPlat = GM_createMenu.list[0].curr === 'off' ? 'caiyun' : 'google' | |
} | |
// استبدل الترجمات الأصلية للتنفيذ | |
function transText(cb) { | |
let source = '', | |
result = '', | |
chunkArr = [], | |
chunkSize = 0, | |
count = 0 | |
// قم بإزالة فواصل الأسطر لكل تعليق ورتبها حسب السطر. | |
sourceSub.forEach((e) => source += e.replace(/\r?\n|\r/g, ' ') + '\n') | |
// إذا كانت الأحرف طويلة جدا، فسيفشل التقديم ويترجم إلى أجزاء. | |
// أعد حجم الكتلة واربطه في رد الاتصال.TODO: تم تحسينه إلى promise | |
chunkSize = chunkTrans(source, (data, index) => { | |
count++ | |
chunkArr[index] = data // أعد المصفوفة وفقا للإيداع الأصلي للتقسيم. | |
// تم استرداد جميع النصوص المترجمة. | |
if (count >= chunkSize) { | |
result = chunkArr.join('\n') // تسلسل نتائج الترجمة المقسمة | |
const subtitleTrans = result.split('\n') // قم بتقسيم كل تعليق توضيحي | |
// اضغط على مزيج اللغة الإنجليزية العلوي والأوسط والسفلي وقم بتعديله مباشرة إلى كائن الترجمة الأصلي. | |
subtitleTrans.forEach((item, idx) => { | |
const el = __CUES[idx] | |
el.text = `${item}\n${el.text}` | |
}) | |
cb && cb(result) | |
} | |
}) | |
} | |
// ترجمة البلوك | |
function chunkTrans(str, callback) { | |
let textArr = [], | |
count = 1 | |
//حظر ترجمة أكثر من 5000 حرف | |
if (str.length > 5000) { | |
let strArr = str.split('\n'), | |
i = 0 | |
strArr.forEach(str => { | |
textArr[i] = textArr[i] || '' | |
// إذا تجاوز الطول 5000 حرف بعد إضافة هذا السطر، فسيتم تقسيمه إلى سلاسل. | |
if ((textArr[i] + str).length > (i + 1) * 5000) { | |
i++ | |
textArr[i] = '' | |
} | |
textArr[i] += str + '\n' | |
}) | |
count = i + 1 // عدد سلاسل السجلات | |
} else { | |
textArr[0] = str | |
} | |
// اجتياز كل قطعة للترجمة بشكل منفصل. | |
textArr.forEach(function(text, index) { | |
doTrans({ | |
transPlat, | |
text: text.trim(), | |
index: index | |
}, callback) | |
}) | |
return count // أعد عدد السلاسل | |
} | |
// تنفيذ الترجمة السحابية الملونة | |
function doTrans(sourceObj, callback) { | |
switch (transPlat) { | |
// كايون للترجمة | |
case 'caiyun': | |
// قم بتهيئة واجهة السحابة الملونة قبل إجراء عمليات الترجمة. | |
initCaiyun() | |
.then(({ | |
browser_id, | |
jwt | |
}) => { | |
const data = { | |
source: sourceObj.text.split('\n'), // الترجمة حسب السطر | |
trans_type: 'en2zh', | |
request_id: 'web_fanyi', | |
media: 'text', | |
os_type: 'web', | |
dict: false, | |
cached: true, | |
replaced: true, | |
browser_id | |
} | |
Request('https://api.interpreter.caiyunai.com/v1/translator', { | |
method: 'POST', | |
headers: { | |
'accept': 'application/json', | |
'content-type': 'application/json charset=UTF-8', | |
'X-Authorization': 'token:qgemv4jr1y38jyq6vhvi', | |
'T-Authorization': jwt | |
}, | |
data: JSON.stringify(data), | |
}) | |
.then(function(response) { | |
var result = JSON.parse(response.responseText) | |
// استخراج منطق التشفير وفك التشفير من ترجمة إصدار الويب Caiyun | |
// ascii | |
const __CHAR_MAP_1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' | |
// جدول رمز كلمة مرور قيصر | |
const __CHAR_MAP_2 = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm' | |
const index = t => __CHAR_MAP_1.indexOf(t) // اجتياز النص المشفر فهرس الإرجاع في الأبجدية إرجاع غير حرف-1 | |
const encode = e => { | |
return e.split('') | |
.map(t => index(t) > -1 ? __CHAR_MAP_2[index(t)] : t) // إذا كانت قيمة الإرجاع أكبر من -1، خذ القيمة الأعلى للأرقام المقابلة لجدول كلمة المرور، وإلا فأرجع نفسها واربطها في سلسلة جديدة) | |
.join('').replace(/[-_]/g, e => '-' === e ? '+' : '/') | |
.replace(/[^A-Za-z0-9\+\/]/g, '') // 去除非法字符 | |
} | |
const btou = e => { | |
return e.replace(/[À-ß][-¿]|[à-ï][-¿]{2}|[ð-÷][-¿]{3}/g, e => { | |
switch (e.length) { | |
case 4: | |
const t = ((7 & e.charCodeAt(0)) << 18 | (63 & e.charCodeAt(1)) << 12 | (63 & e.charCodeAt(2)) << 6 | 63 & e.charCodeAt(3)) - 65536 | |
return String.fromCharCode(55296 + (t >>> 10)) + String.fromCharCode(56320 + (1023 & t)) | |
case 3: | |
return String.fromCharCode((15 & e.charCodeAt(0)) << 12 | (63 & e.charCodeAt(1)) << 6 | 63 & e.charCodeAt(2)) | |
default: | |
return String.fromCharCode((31 & e.charCodeAt(0)) << 6 | 63 & e.charCodeAt(1)) | |
} | |
}) | |
} | |
const encodeArr = result.target.map(words => { | |
const base64 = encode(words) // '6Vh55c6p' -> '6Iu55p6c' | |
return btou(atob(base64)) // '6Iu55p6c' -> 'è¹æ' -> 'تُفّاح' | |
}) | |
callback(encodeArr.join('\n'), sourceObj.index) // قم بإجراء رد اتصال واربطه في رد الاتصال. | |
}) | |
}) | |
break | |
// ترجمة جوجل | |
case 'google': | |
Request('https://translate.google.com/translate_a/t', { | |
method: 'GET', | |
headers: { | |
'accept': 'application/json', | |
'content-type': 'application/json charset=UTF-8', | |
}, | |
params: { | |
client: 'dict-chrome-ex', | |
sl: 'auto', | |
tl: 'ar-sa', | |
q: sourceObj.text, | |
} | |
}) | |
.then(function(response) { | |
const result = JSON.parse(response.responseText) | |
callback(result[0], sourceObj.index) // قم بإجراء رد اتصال واربطه في رد الاتصال. | |
}) | |
break | |
} | |
} | |
function initCaiyun() { | |
const state = { | |
browser_id: '', | |
jwt: '' | |
} | |
return new Promise(function(resolve, reject) { | |
if (state.browser_id && state.jwt) { | |
resolve(state) | |
} else { | |
Fingerprint2 && Fingerprint2.get({}, function(components) { | |
const values = components.map(component => component.value) | |
const browser_id = Fingerprint2.x64hash128(values.join(''), 233) | |
Request('https://api.interpreter.caiyunai.com/v1/user/jwt/generate', { | |
method: 'POST', | |
headers: { | |
'accept': 'application/json', | |
'content-type': 'application/json charset=UTF-8', | |
'X-Authorization': 'token:qgemv4jr1y38jyq6vhvi' | |
}, | |
data: JSON.stringify({ | |
'browser_id': browser_id | |
}) | |
}) | |
.then(function(response) { | |
const result = JSON.parse(response.responseText) | |
resolve({ | |
browser_id, | |
jwt: result.jwt | |
}) | |
}) | |
.catch(function(error) { | |
reject(error) | |
}) | |
}) | |
} | |
}) | |
} | |
function clone(item) { | |
if (!item) { | |
return item; | |
} // null, undefined values check | |
var types = [Number, String, Boolean], | |
result; | |
// normalizing primitives if someone did new String('aaa'), or new Number('444'); | |
types.forEach(function(type) { | |
if (item instanceof type) { | |
result = type(item); | |
} | |
}); | |
if (typeof result == "undefined") { | |
if (Object.prototype.toString.call(item) === "[object Array]") { | |
result = []; | |
item.forEach(function(child, index, array) { | |
result[index] = clone(child); | |
}); | |
} else if (typeof item == "object") { | |
// testing that this is DOM | |
if (item.nodeType && typeof item.cloneNode == "function") { | |
result = item.cloneNode(true); | |
} else if (!item.prototype) { // check that this is a literal | |
if (item instanceof Date) { | |
result = new Date(item); | |
} else { | |
// it is an object literal | |
result = {}; | |
for (var i in item) { | |
result[i] = clone(item[i]); | |
} | |
} | |
} else { | |
// depending what you would like here, | |
// just keep the reference, or create new object | |
if (false && item.constructor) { | |
// would not advice to do that, reason? Read below | |
result = new item.constructor(); | |
} else { | |
result = item; | |
} | |
} | |
} else { | |
result = item; | |
} | |
} | |
return result; | |
} | |
function addStyle(css) { | |
if (typeof GM_addStyle != 'undefined') { | |
GM_addStyle(css) | |
} else if (typeof PRO_addStyle != 'undefined') { | |
PRO_addStyle(css) | |
} else { | |
var node = document.createElement('style') | |
node.type = 'text/css' | |
node.appendChild(document.createTextNode(css)) | |
var heads = document.getElementsByTagName('head') | |
if (heads.length > 0) { | |
heads[0].appendChild(node) | |
} else { | |
// no head yet, stick it whereever | |
document.documentElement.appendChild(node) | |
} | |
} | |
} | |
function Request(url, opt = {}) { | |
const originURL = new URL(url) | |
const originParams = originURL.searchParams | |
new URLSearchParams(opt.params).forEach((value, key) => originParams.append(key, value)) | |
const newQS = originParams.toString() !== '' ? `?${originParams.toString()}` : '' | |
const newURL = `${originURL.origin}${originURL.pathname}${newQS}` | |
Object.assign(opt, { | |
url: newURL, | |
timeout: 20000, | |
responseType: 'json' | |
}) | |
return new Promise((resolve, reject) => { | |
opt.onerror = opt.ontimeout = reject | |
opt.onload = resolve | |
GM_xmlhttpRequest(opt) | |
}) | |
} | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment