Skip to content

Instantly share code, notes, and snippets.

  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save imAbdelhadi/91f11ad24ca670b3a2eb788666a6032b to your computer and use it in GitHub Desktop.
LinkedIn Learning ترجمة دورات لينكدإن ليرننج إلى اللغة العربية آليا
// طريقة التشغيل من موقعي على الرابط التالي: 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