GitHub Russian Translation

Translates GitHub websites into Russian

  1. // ==UserScript==
  2. // @name GitHub Russian Translation
  3. // @name:ru Русификатор GitHub
  4. // @author Deflecta
  5. // @contributionURL https://boosty.to/rushanm
  6. // @description Translates GitHub websites into Russian
  7. // @description:ru Переводит сайты GitHub на русский язык
  8. // @grant none
  9. // @homepageURL https://github.com/RushanM/GitHub-Russian-Translation
  10. // @icon https://github.githubassets.com/favicons/favicon.png
  11. // @license MIT
  12. // @match https://github.com/*
  13. // @match https://github.blog/*
  14. // @match https://education.github.com/*
  15. // @run-at document-end
  16. // @namespace githubrutraslation
  17. // @supportURL https://github.com/RushanM/GitHub-Russian-Translation/issues
  18. // @version 1-B27
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23.  
  24. const interFontLink = document.createElement('link');
  25. interFontLink.rel = 'stylesheet';
  26. interFontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap';
  27. document.head.appendChild(interFontLink);
  28.  
  29. // Загружаем переводы из удалённого файла rus_p.json и объединяем все секции
  30. let translations = {};
  31. fetch("https://raw.githubusercontent.com/RushanM/GitHub-Russian-Translation/refs/heads/master/%D0%9E%D0%B1%D1%89%D0%B5%D0%B5/rus_p.json")
  32. .then(response => response.json())
  33. .then(data => {
  34. // Сохраняем перевод из dashboard для Chat with Copilot
  35. window.dashboardCopilotTranslation = data.dashboard["Chat with Copilot"];
  36. // Сохраняем перевод из dashboard для Home
  37. window.dashboardHomeTranslation = data.dashboard["Home"];
  38. translations = Object.assign(
  39. {},
  40. data.dashboard,
  41. data.search,
  42. data.left_sidebar,
  43. data.settings,
  44. data.repo_tabs,
  45. data.copilot,
  46. data.createnew,
  47. data.right_sidebar,
  48. data.copilot_openwith,
  49. data.general
  50. );
  51. runTranslation();
  52. });
  53.  
  54. function runTranslation() {
  55. function getRepositoriesTranslation(count) {
  56. if (count === 1) return `${count} репозиторий`;
  57. if (count >= 2 && count <= 4) return `${count} репозитория`;
  58. return `${count} репозиториев`;
  59. }
  60.  
  61. function formatStarCount() {
  62. const starCounters = document.querySelectorAll('.Counter.js-social-count');
  63. starCounters.forEach(counter => {
  64. let text = counter.textContent.trim();
  65. if (text.includes('k')) {
  66. text = text.replace('.', ',').replace('k', 'К');
  67. counter.textContent = text;
  68. }
  69. });
  70. }
  71.  
  72. // Функция для перевода абсолютного времени во всплывающей подсказке, например:
  73. // «Feb 24, 2025, 3:09 PM GMT+3» → «24 февраля 2025, 15:09 по московскому времени»
  74. function translateAbsoluteTime(text) {
  75. // Маппирование месяцев берём из файла с переводами
  76. const monthMapping = translations.months || {
  77. Jan: 'января',
  78. Feb: 'февраля',
  79. Mar: 'марта',
  80. Apr: 'апреля',
  81. May: 'мая',
  82. Jun: 'июня',
  83. Jul: 'июля',
  84. Aug: 'августа',
  85. Sep: 'сентября',
  86. Oct: 'октября',
  87. Nov: 'ноября',
  88. Dec: 'декабря'
  89. };
  90.  
  91. // Регулярное выражение для извлечения компонентов времени
  92. // Пример: Feb 24, 2025, 3:09 PM GMT+3
  93. const regex = /^([A-Z][a-z]{2}) (\d{1,2}), (\d{4}), (\d{1,2}):(\d{2})\s*(AM|PM)\s*GMT\+3$/;
  94. const match = text.match(regex);
  95. if (match) {
  96. const monthEn = match[1];
  97. const day = match[2];
  98. const year = match[3];
  99. let hour = parseInt(match[4], 10);
  100. const minute = match[5];
  101. const period = match[6];
  102.  
  103. // Преобразование в 24-часовой формат
  104. if (period === 'PM' && hour !== 12) {
  105. hour += 12;
  106. } else if (period === 'AM' && hour === 12) {
  107. hour = 0;
  108. }
  109. // Форматирование часов с ведущим нулём
  110. const hourStr = hour < 10 ? '0' + hour : hour.toString();
  111. const monthRu = monthMapping[monthEn] || monthEn;
  112.  
  113. // Используем перевод из файла переводов
  114. const byMoscowTime = translations.time?.by_moscow_time || "по московскому времени";
  115. return `${day} ${monthRu} ${year}, ${hourStr}:${minute} ${byMoscowTime}`;
  116. }
  117. return text;
  118. }
  119.  
  120. // Функция для перевода элементов <relative-time>
  121. function translateRelativeTimes() {
  122. const timeElements = document.querySelectorAll('relative-time');
  123. timeElements.forEach(el => {
  124. // Если элемент уже переведён в основном DOM, проверяем его теневой DOM
  125. if (el.getAttribute('data-translated')) {
  126. // Проверяем наличие теневого корня и пытаемся перевести его содержимое
  127. if (el.shadowRoot) {
  128. // Получаем текстовые узлы из теневого корня
  129. const shadowTextNodes = [];
  130.  
  131. // Функция для рекурсивного обхода теневого DOM
  132. function collectTextNodes(node) {
  133. if (node.nodeType === Node.TEXT_NODE) {
  134. shadowTextNodes.push(node);
  135. } else if (node.childNodes) {
  136. node.childNodes.forEach(childNode => {
  137. collectTextNodes(childNode);
  138. });
  139. }
  140. }
  141. // Обходим теневой DOM
  142. collectTextNodes(el.shadowRoot);
  143.  
  144. // Переводим каждый текстовый узел
  145. shadowTextNodes.forEach(textNode => {
  146. const originalText = textNode.textContent.trim();
  147. // Ищем паттерны времени
  148. const hoursAgoMatch = originalText.match(/(\d+)\s+hours?\s+ago/);
  149. const minutesAgoMatch = originalText.match(/(\d+)\s+minutes?\s+ago/);
  150. const daysAgoMatch = originalText.match(/(\d+)\s+days?\s+ago/);
  151. const weeksAgoMatch = originalText.match(/(\d+)\s+weeks?\s+ago/);
  152. const lastWeekMatch = originalText.match(/last\s+week/i);
  153. const yesterdayMatch = originalText.match(/yesterday/i);
  154. const dateMatch = originalText.match(/([A-Za-z]+)\s+(\d{1,2}),\s+(\d{4})\s+(\d{1,2}):(\d{2})/i);
  155. const shortDateMatch = originalText.match(/on\s+([A-Za-z]{3})\s+(\d{1,2})/i);
  156.  
  157. if (hoursAgoMatch) {
  158. const hours = parseInt(hoursAgoMatch[1], 10);
  159. let translatedText;
  160.  
  161. // Правильное склонение
  162. if (hours === 1) {
  163. translatedText = translations.time?.hour_singular || "1 час назад";
  164. } else if (hours >= 2 && hours <= 4) {
  165. translatedText = (translations.time?.hour_few || "{count} часа назад").replace("{count}", hours);
  166. } else {
  167. translatedText = (translations.time?.hour_many || "{count} часов назад").replace("{count}", hours);
  168. }
  169.  
  170. textNode.textContent = textNode.textContent.replace(
  171. /(\d+)\s+hours?\s+ago/,
  172. translatedText
  173. );
  174. } else if (daysAgoMatch) {
  175. const days = parseInt(daysAgoMatch[1], 10);
  176. let translatedText;
  177.  
  178. // Правильное склонение
  179. if (days === 1) {
  180. translatedText = translations.time?.day_singular || "1 день назад";
  181. } else if (days >= 2 && days <= 4) {
  182. translatedText = (translations.time?.day_few || "{count} дня назад").replace("{count}", days);
  183. } else {
  184. translatedText = (translations.time?.day_many || "{count} дней назад").replace("{count}", days);
  185. }
  186.  
  187. textNode.textContent = textNode.textContent.replace(
  188. /(\d+)\s+days?\s+ago/,
  189. translatedText
  190. );
  191. } else if (weeksAgoMatch) {
  192. const weeks = parseInt(weeksAgoMatch[1], 10);
  193. let translatedText;
  194.  
  195. // Правильное склонение
  196. if (weeks === 1) {
  197. translatedText = translations.time?.week_singular || "1 неделю назад";
  198. } else if (weeks >= 2 && weeks <= 4) {
  199. translatedText = (translations.time?.week_few || "{count} недели назад").replace("{count}", weeks);
  200. } else {
  201. translatedText = (translations.time?.week_many || "{count} недель назад").replace("{count}", weeks);
  202. }
  203.  
  204. textNode.textContent = textNode.textContent.replace(
  205. /(\d+)\s+weeks?\s+ago/,
  206. translatedText
  207. );
  208. } else if (lastWeekMatch) {
  209. textNode.textContent = textNode.textContent.replace(
  210. /last\s+week/i,
  211. translations.time?.last_week || "на прошлой неделе"
  212. );
  213. } else if (yesterdayMatch) {
  214. textNode.textContent = textNode.textContent.replace(
  215. /yesterday/i,
  216. translations.time?.yesterday || "вчера"
  217. );
  218. } else if (shortDateMatch) {
  219. // Обработка формата «on Mar 13»
  220. const monthEn = shortDateMatch[1];
  221. const day = shortDateMatch[2];
  222.  
  223. // Используем словарь месяцев из файла переводов
  224. const monthRu = translations.months?.[monthEn] || monthEn;
  225. const translatedDate = `${day} ${monthRu}`;
  226.  
  227. textNode.textContent = textNode.textContent.replace(
  228. /on\s+[A-Za-z]{3}\s+\d{1,2}/i,
  229. `${day} ${monthRu}`
  230. );
  231. } else if (dateMatch) {
  232. // Если у нас полная дата в формате «April 11, 2025 10:27»
  233. const monthEn = dateMatch[1];
  234. const day = dateMatch[2];
  235. const year = dateMatch[3];
  236. const hour = dateMatch[4];
  237. const minute = dateMatch[5];
  238.  
  239. // Используем словарь месяцев из файла переводов
  240. const monthRu = translations.months?.[monthEn] || monthEn;
  241. const translatedDate = `${day} ${monthRu} ${year} ${hour}:${minute}`;
  242.  
  243. textNode.textContent = textNode.textContent.replace(
  244. /[A-Za-z]+\s+\d{1,2},\s+\d{4}\s+\d{1,2}:\d{2}/i,
  245. translatedDate
  246. );
  247. } else if (minutesAgoMatch) {
  248. const minutes = parseInt(minutesAgoMatch[1], 10);
  249. let translatedText;
  250.  
  251. // Правильное склонение
  252. if (minutes === 1) {
  253. translatedText = translations.time?.minute_singular || "1 минуту назад";
  254. } else if (minutes >= 2 && minutes <= 4) {
  255. translatedText = (translations.time?.minute_few || "{count} минуты назад").replace("{count}", minutes);
  256. } else if (minutes >= 5 && minutes <= 20) {
  257. translatedText = (translations.time?.minute_many || "{count} минут назад").replace("{count}", minutes);
  258. } else {
  259. // Для чисел 21, 31, 41… используем окончание как для 1
  260. const lastDigit = minutes % 10;
  261. const lastTwoDigits = minutes % 100;
  262.  
  263. if (lastDigit === 1 && lastTwoDigits !== 11) {
  264. translatedText = (translations.time?.minute_singular || "1 минуту назад").replace("1", minutes);
  265. } else if (lastDigit >= 2 && lastDigit <= 4 && (lastTwoDigits < 10 || lastTwoDigits > 20)) {
  266. translatedText = (translations.time?.minute_few || "{count} минуты назад").replace("{count}", minutes);
  267. } else {
  268. translatedText = (translations.time?.minute_many || "{count} минут назад").replace("{count}", minutes);
  269. }
  270. }
  271.  
  272. textNode.textContent = textNode.textContent.replace(
  273. /(\d+)\s+minutes?\s+ago/,
  274. translatedText
  275. );
  276. }
  277. });
  278. }
  279. return;
  280. }
  281.  
  282. // Перевод всплывающей подсказки, если атрибут title существует
  283. if (el.hasAttribute('title')) {
  284. const originalTitle = el.getAttribute('title');
  285. el.setAttribute('title', translateAbsoluteTime(originalTitle));
  286. }
  287.  
  288. // Обработка текста даты/времени внутри элемента
  289. const originalText = el.textContent.trim();
  290.  
  291. // Обработка относительного времени «x hours ago», «x minutes ago» и т. д.
  292. const hoursAgoMatch = originalText.match(/(\d+)\s+hours?\s+ago/);
  293. const minutesAgoMatch = originalText.match(/(\d+)\s+minutes?\s+ago/);
  294. const onDateMatch = originalText.match(/on\s+([A-Za-z]+\s+\d+,\s+\d+)/);
  295.  
  296. if (hoursAgoMatch) {
  297. const hours = parseInt(hoursAgoMatch[1], 10);
  298. let translatedText;
  299.  
  300. // Правильное склонение
  301. if (hours === 1) {
  302. translatedText = "1 час назад";
  303. } else if (hours >= 2 && hours <= 4) {
  304. translatedText = hours + " часа назад";
  305. } else {
  306. translatedText = hours + " часов назад";
  307. }
  308.  
  309. el.textContent = translatedText;
  310. } else if (minutesAgoMatch) {
  311. const minutes = parseInt(minutesAgoMatch[1], 10);
  312. let translatedText;
  313.  
  314. // Правильное склонение
  315. if (minutes === 1) {
  316. translatedText = "1 минуту назад";
  317. } else if (minutes >= 2 && minutes <= 4) {
  318. translatedText = minutes + " минуты назад";
  319. } else if (minutes >= 5 && minutes <= 20) {
  320. translatedText = minutes + " минут назад";
  321. } else {
  322. // Для чисел 21, 31, 41… используем окончание как для 1
  323. const lastDigit = minutes % 10;
  324. const lastTwoDigits = minutes % 100;
  325.  
  326. if (lastDigit === 1 && lastTwoDigits !== 11) {
  327. translatedText = minutes + " минуту назад";
  328. } else if (lastDigit >= 2 && lastDigit <= 4 && (lastTwoDigits < 10 || lastTwoDigits > 20)) {
  329. translatedText = minutes + " минуты назад";
  330. } else {
  331. translatedText = minutes + " минут назад";
  332. }
  333. }
  334.  
  335. el.textContent = translatedText;
  336. } else if (onDateMatch) {
  337. // Обрабатываем формат «on Apr 12, 2025»
  338. // Переводим английское название месяца на русский
  339. const dateText = onDateMatch[1];
  340. const monthMapping = {
  341. Jan: 'января',
  342. Feb: 'февраля',
  343. Mar: 'марта',
  344. Apr: 'апреля',
  345. May: 'мая',
  346. Jun: 'июня',
  347. Jul: 'июля',
  348. Aug: 'августа',
  349. Sep: 'сентября',
  350. Oct: 'октября',
  351. Nov: 'ноября',
  352. Dec: 'декабря'
  353. };
  354.  
  355. // Регулярное выражение для поиска и замены месяца в строке даты
  356. const monthRegex = /([A-Za-z]{3})\s+(\d{1,2}),\s+(\d{4})/;
  357. const dateMatch = dateText.match(monthRegex);
  358.  
  359. if (dateMatch) {
  360. const monthEn = dateMatch[1];
  361. const day = dateMatch[2];
  362. const year = dateMatch[3];
  363. const monthRu = monthMapping[monthEn] || monthEn;
  364.  
  365. el.textContent = ${day} ${monthRu} ${year}`;
  366. } else {
  367. el.textContent = "в " + dateText;
  368. }
  369. }
  370.  
  371. // Пробуем перехватывать события мутации теневого корня
  372. if (window.ShadowRoot && !el.getAttribute('data-shadow-observed')) {
  373. try {
  374. // Пытаемся получить доступ к теневому DOM
  375. const shadowRoot = el.shadowRoot;
  376. if (shadowRoot) {
  377. // Добавляем наблюдатель за изменениями в теневом DOM
  378. const shadowObserver = new MutationObserver((mutations) => {
  379. mutations.forEach(mutation => {
  380. if (mutation.type === 'childList' || mutation.type === 'characterData') {
  381. // Проходим по всем текстовым узлам в теневом DOM
  382. const textNodes = [];
  383. const walk = document.createTreeWalker(shadowRoot, NodeFilter.SHOW_TEXT);
  384.  
  385. let node;
  386. while (node = walk.nextNode()) {
  387. textNodes.push(node);
  388. }
  389.  
  390. textNodes.forEach(textNode => {
  391. const originalText = textNode.textContent;
  392. // Ищем паттерны времени
  393. if (originalText.match(/(\d+)\s+hours?\s+ago/)) {
  394. const hours = parseInt(originalText.match(/(\d+)\s+hours?\s+ago/)[1], 10);
  395. let translatedText;
  396. // Правильное склонение
  397. if (hours === 1) {
  398. translatedText = translations.time?.hour_singular || "1 час назад";
  399. } else if (hours >= 2 && hours <= 4) {
  400. translatedText = (translations.time?.hour_few || "{count} часа назад").replace("{count}", hours);
  401. } else {
  402. translatedText = (translations.time?.hour_many || "{count} часов назад").replace("{count}", hours);
  403. }
  404.  
  405. textNode.textContent = translatedText;
  406. } else if (originalText.match(/(\d+)\s+days?\s+ago/)) {
  407. const days = parseInt(originalText.match(/(\d+)\s+days?\s+ago/)[1], 10);
  408. let translatedText;
  409.  
  410. // Правильное склонение для русского языка
  411. if (days === 1) {
  412. translatedText = translations.time?.day_singular || "1 день назад";
  413. } else if (days >= 2 && days <= 4) {
  414. translatedText = (translations.time?.day_few || "{count} дня назад").replace("{count}", days);
  415. } else {
  416. translatedText = (translations.time?.day_many || "{count} дней назад").replace("{count}", days);
  417. }
  418.  
  419. textNode.textContent = translatedText;
  420. } else if (originalText.match(/yesterday/i)) {
  421. textNode.textContent = translations.time?.yesterday || "вчера";
  422. } else if (originalText.match(/([A-Za-z]+)\s+(\d{1,2}),\s+(\d{4})\s+(\d{1,2}):(\d{2})/i)) {
  423. const match = originalText.match(/([A-Za-z]+)\s+(\d{1,2}),\s+(\d{4})\s+(\d{1,2}):(\d{2})/i); const monthEn = match[1];
  424. const day = match[2];
  425. const year = match[3];
  426. const hour = match[4];
  427. const minute = match[5];
  428.  
  429. // Используем словарь месяцев из файла переводов
  430. const monthRu = translations.months?.[monthEn] || monthEn;
  431. textNode.textContent = `${day} ${monthRu} ${year} ${hour}:${minute}`;
  432. } else if (originalText.match(/(\d+)\s+minutes?\s+ago/)) {
  433. const minutes = parseInt(originalText.match(/(\d+)\s+minutes?\s+ago/)[1], 10);
  434. let translatedText;
  435.  
  436. // Правильное склонение
  437. if (minutes === 1) {
  438. translatedText = translations.time?.minute_singular || "1 минуту назад";
  439. } else if (minutes >= 2 && minutes <= 4) {
  440. translatedText = (translations.time?.minute_few || "{count} минуты назад").replace("{count}", minutes);
  441. } else {
  442. translatedText = (translations.time?.minute_many || "{count} минут назад").replace("{count}", minutes);
  443. }
  444.  
  445. textNode.textContent = translatedText;
  446. }
  447. });
  448. }
  449. });
  450. });
  451.  
  452. shadowObserver.observe(shadowRoot, {
  453. childList: true,
  454. characterData: true,
  455. subtree: true
  456. });
  457.  
  458. el.setAttribute('data-shadow-observed', 'true');
  459. }
  460. } catch (error) {
  461. console.error('[Русификатор Гитхаба] Ошибка при работе с теневым DOM:', error);
  462. }
  463. }
  464.  
  465. // Отмечаем элемент как переведённый
  466. el.setAttribute('data-translated', 'true');
  467. });
  468. }
  469.  
  470. // функция для проверки, находится ли элемент в контейнере, где перевод нежелателен
  471. function isExcludedElement(el) {
  472. // Если элемент находится внутри заголовков Markdown, то не переводим
  473. if (el.closest('.markdown-heading')) return true;
  474. // Если элемент находится внутри ячейки с названием каталога, то не переводим
  475. if (el.closest('.react-directory-filename-column')) return true;
  476. return false;
  477. }
  478.  
  479. // Функция для перевода блока GitHub Education
  480. function translateEducationExperience() {
  481. document.querySelectorAll('.experience__action-item h3').forEach(el => {
  482. if (el.textContent.includes('Add') && el.textContent.includes('repositories to a list')) {
  483. el.innerHTML = el.innerHTML.replace(
  484. /Add .* repositories to a list/,
  485. 'Добавьте репозитории, на которые поставили звезду, в список'
  486. );
  487. }
  488. });
  489.  
  490. document.querySelectorAll('.experience__action-item p.mt-4.f4').forEach(el => {
  491. if (el.textContent.includes('To complete this task, create a list with at least 3')) {
  492. el.innerHTML = el.innerHTML.replace(
  493. /To complete this task, create a list with at least 3 .* repos with the list name 'My Repo Watchlist'./,
  494. 'Чтобы выполнить это задание, создайте список, содержащий не менее трёх репозиториев, на которые вы поставили звезду, с названием «My Repo Watchlist».'
  495. );
  496. }
  497. });
  498.  
  499. document.querySelectorAll('.experience__actions-items-labels .Label--attention').forEach(el => {
  500. if (el.textContent.trim() === 'Incomplete') {
  501. el.textContent = 'Не выполнено';
  502. }
  503. });
  504.  
  505. document.querySelectorAll('.experience__actions-items-labels .Label--secondary').forEach(el => {
  506. if (el.textContent.trim() === 'List') {
  507. el.textContent = 'Список';
  508. }
  509. });
  510.  
  511. document.querySelectorAll('.experience__cta .Button--primary .Button-label').forEach(el => {
  512. if (el.textContent.trim() === 'See detailed instructions') {
  513. el.textContent = 'Подробные инструкции';
  514. }
  515. });
  516.  
  517. document.querySelectorAll('.experience__cta .Button--secondary .Button-label').forEach(el => {
  518. if (el.textContent.trim() === 'Mark complete') {
  519. el.textContent = 'Отметить как выполненное';
  520. }
  521. });
  522. }
  523.  
  524. function translateTextContent() {
  525. const elements = document.querySelectorAll(
  526. '.ActionList-sectionDivider-title, .ActionListItem-label, span[data-content], ' +
  527. '.AppHeader-context-item-label, #qb-input-query, .Truncate-text, h2, button, ' +
  528. '.Label, a, img[alt], .Box-title, .post__content p, .post__content li, .Button-label, ' +
  529. '.prc-ActionList-GroupHeading-eahp0'
  530. );
  531.  
  532. elements.forEach(el => {
  533. // Если элемент подпадает под исключения, пропускаем его
  534. if (isExcludedElement(el)) return;
  535.  
  536. if (el.tagName === 'IMG' && el.alt.trim() in translations) {
  537. el.alt = translations[el.alt.trim()];
  538. } else if (el.childElementCount === 0) {
  539. const text = el.textContent.trim();
  540. if (translations[text]) {
  541. el.textContent = translations[text];
  542. } else {
  543. const match = text.match(/^(\d+) repositories$/);
  544. if (match) {
  545. const count = parseInt(match[1], 10);
  546. el.textContent = getRepositoriesTranslation(count);
  547. } else if (text === 'added a repository to') {
  548. el.textContent = translations['added a repository to'];
  549. }
  550. }
  551. } else {
  552. // Часть функции translateTextContent(), отвечающая за обработку элементов с дочерними элементами
  553. if (el.childElementCount > 0) {
  554. // Сборка текстового содержания с учётом дочерних элементов
  555. let text = '';
  556. el.childNodes.forEach(node => {
  557. if (node.nodeType === Node.TEXT_NODE) {
  558. text += node.textContent;
  559. } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'KBD') {
  560. text += '/'; // Добавление символа «/» из <kbd>
  561. }
  562. });
  563. text = text.trim();
  564. if (translations[text]) {
  565. // Создание нового фрагмента с переводом и сохранение тега <kbd>, если перевод соответствует шаблону
  566. const newFragment = document.createDocumentFragment();
  567. const parts = translations[text].split('<kbd class="AppHeader-search-kbd">/</kbd>');
  568. newFragment.append(document.createTextNode(parts[0]));
  569. // Добавлять тег <kbd> только если есть вторая часть перевода
  570. if (parts.length > 1 && parts[1] !== undefined) {
  571. const kbd = document.createElement('kbd');
  572. kbd.className = 'AppHeader-search-kbd';
  573. kbd.textContent = '/';
  574. newFragment.append(kbd);
  575. newFragment.append(document.createTextNode(parts[1]));
  576. }
  577. // Очистка элемента и вставка нового контента
  578. el.innerHTML = '';
  579. el.appendChild(newFragment);
  580. }
  581. el.childNodes.forEach(node => {
  582. if (node.nodeType === Node.TEXT_NODE) {
  583. const originalText = node.textContent;
  584. const trimmed = originalText.trim();
  585. if (translations[trimmed]) {
  586. node.textContent = translations[trimmed];
  587. } else if (originalText.includes("starred")) {
  588. node.textContent = originalText.replace("starred", translations["starred"]);
  589. } else if (originalText.includes("added a repository to")) {
  590. node.textContent = originalText.replace("added a repository to", translations['added a repository to']);
  591. } else if (originalText.includes("Notifications")) {
  592. node.textContent = originalText.replace("Notifications", translations['Notifications']);
  593. }
  594. }
  595. });
  596. // Последный выхват строчек
  597. if (/\bstarred\b/.test(el.innerHTML) && !el.innerHTML.includes('starred-button-icon')) {
  598. el.innerHTML = el.innerHTML.replace(/\bstarred\b/g, function (match) {
  599. return translations["starred"];
  600. });
  601. }
  602. if (/\badded a repository to\b/.test(el.innerHTML)) {
  603. el.innerHTML = el.innerHTML.replace(/\badded a repository to\b/g, translations['added a repository to']);
  604. }
  605. if (/\bNotifications\b/.test(el.innerHTML)) {
  606. el.innerHTML = el.innerHTML.replace(/\bNotifications\b/g, translations['Notifications']);
  607. }
  608. } else {
  609. // Сначала каждый узел
  610. el.childNodes.forEach(node => {
  611. if (node.nodeType === Node.TEXT_NODE) {
  612. let originalText = node.textContent;
  613. // Переводы
  614. originalText = originalText.replace(/\bstarred\b/g, translations["starred"]);
  615. originalText = originalText.replace(/\badded a repository to\b/g, translations['added a repository to']);
  616. originalText = originalText.replace(/\bNotifications\b/g, translations['Notifications']);
  617. node.textContent = originalText;
  618. }
  619. });
  620.  
  621. // Если всё ещё остаётся, заменить внутренний HTML
  622. if (/\bstarred\b/.test(el.innerHTML)) {
  623. el.innerHTML = el.innerHTML.replace(/\bstarred\b/g, translations["starred"]);
  624. }
  625. if (/\badded a repository to\b/.test(el.innerHTML)) {
  626. el.innerHTML = el.innerHTML.replace(/\badded a repository to\b/g, translations['added a repository to']);
  627. }
  628. if (/\bNotifications\b/.test(el.innerHTML)) {
  629. el.innerHTML = el.innerHTML.replace(/\bNotifications\b/g, translations['Notifications']);
  630. }
  631. }
  632. }
  633. });
  634. formatStarCount();
  635. translateRelativeTimes();
  636.  
  637. document.querySelectorAll('.Button-label').forEach(btn => {
  638. if (btn.textContent.trim() === "New") {
  639. btn.textContent = "Создать";
  640. }
  641. });
  642. }
  643.  
  644. function translateCopilotPreview() {
  645. const askCopilotPlaceholder = document.querySelector('.copilotPreview__input[placeholder="Ask Copilot"]');
  646. if (askCopilotPlaceholder && translations['Ask Copilot']) {
  647. askCopilotPlaceholder.setAttribute('placeholder', translations['Ask Copilot']);
  648. }
  649. document.querySelectorAll('.copilotPreview__suggestionButton div').forEach(div => {
  650. const text = div.textContent.trim();
  651. if (translations[text]) {
  652. div.innerHTML = translations[text];
  653. }
  654. });
  655. }
  656.  
  657. function translateAttributes() {
  658. // Перевод placeholder
  659. document.querySelectorAll('input[placeholder]').forEach(el => {
  660. const text = el.getAttribute('placeholder');
  661. if (translations[text]) {
  662. el.setAttribute('placeholder', translations[text]);
  663. }
  664. });
  665. // Перевод aria-label
  666. document.querySelectorAll('[aria-label]').forEach(el => {
  667. const text = el.getAttribute('aria-label');
  668. if (translations[text]) {
  669. el.setAttribute('aria-label', translations[text]);
  670. }
  671. });
  672. }
  673.  
  674. function translateTooltips() {
  675. const copilotChatTooltip = document.querySelector('tool-tip[for="copilot-chat-header-button"]');
  676. if (copilotChatTooltip && copilotChatTooltip.textContent.trim() === 'Chat with Copilot') {
  677. // Используем перевод из dashboard, сохранённый ранее
  678. copilotChatTooltip.textContent = window.dashboardCopilotTranslation;
  679. }
  680.  
  681. document.querySelectorAll('tool-tip[role="tooltip"]').forEach(tooltip => {
  682. const text = tooltip.textContent.trim();
  683. if (translations[text]) {
  684. tooltip.textContent = translations[text];
  685. }
  686. });
  687.  
  688. document.querySelectorAll('.prc-TooltipV2-Tooltip-cYMVY').forEach(tooltip => {
  689. const text = tooltip.textContent.trim();
  690. if (translations[text]) {
  691. tooltip.textContent = translations[text];
  692. }
  693. });
  694. }
  695.  
  696. function translateGitHubEducation() {
  697. const noticeForms = document.querySelectorAll('div.js-notice form.js-notice-dismiss');
  698.  
  699. noticeForms.forEach(form => {
  700. const heading = form.querySelector('h3.h4');
  701. if (heading && heading.textContent.trim() === 'Learn. Collaborate. Grow.') {
  702. heading.textContent = 'Учитесь. Кооперируйтесь. Развивайтесь.';
  703. }
  704.  
  705. const desc = form.querySelector('p.my-3.text-small');
  706. if (desc && desc.textContent.includes('GitHub Education gives you the tools')) {
  707. desc.textContent = 'GitHub Education предоставляет инструменты и поддержку сообщества, чтобы вы могли принимать технологические вызовы и превращать их в возможности. Ваше технологическое будущее начинается здесь!';
  708. }
  709.  
  710. const link = form.querySelector('.Button-label');
  711. if (link && link.textContent.trim() === 'Go to GitHub Education') {
  712. link.textContent = 'Перейти в GitHub Education';
  713. }
  714. });
  715.  
  716. document.querySelectorAll('.h5.color-fg-on-emphasis.text-mono').forEach(el => {
  717. if (el.textContent.trim() === 'LaunchPad') {
  718. el.textContent = translations['LaunchPad'];
  719. }
  720. });
  721.  
  722. document.querySelectorAll('.experience__gradient.experience__title').forEach(el => {
  723. if (el.textContent.trim() === 'Intro to GitHub') {
  724. el.textContent = translations['Intro to GitHub'];
  725. }
  726. });
  727.  
  728. // Шрифт Inter
  729. const educationNavBlock = document.querySelector('.d-flex.flex-justify-center.flex-md-justify-start.pb-5.pb-sm-7');
  730. if (educationNavBlock) {
  731. educationNavBlock.style.fontFamily = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  732. }
  733.  
  734. document.querySelectorAll('header h1.mb-5').forEach(el => {
  735. el.style.fontFamily = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  736. });
  737. }
  738.  
  739. function translateExperienceHeaderItems() {
  740. // Перевод заголовков и текста в блоке experience__header__items
  741. document.querySelectorAll('.experience__header__item .experience__hero__heading').forEach(el => {
  742. const text = el.textContent.trim();
  743. if (translations[text]) {
  744. el.textContent = translations[text];
  745. }
  746. });
  747.  
  748. // Перевод основного текста в блоке experience__header__items
  749. document.querySelectorAll('.experience__header__item p.color-fg-on-emphasis').forEach(el => {
  750. const text = el.textContent.trim();
  751. if (translations[text]) {
  752. el.textContent = translations[text];
  753. }
  754. });
  755.  
  756. // Шрифт Inter ко всему блоку для лучшей поддержки кириллицы
  757. document.querySelectorAll('.color-fg-on-emphasis').forEach(el => {
  758. el.style.fontFamily = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  759. });
  760. }
  761.  
  762. function translateFilterMenu() {
  763. const filterTranslations = {
  764. "Filter": "Фильтр",
  765. "Events": "События",
  766. "Activity you want to see on your feed": "Деятельность, которую вы хотите видеть в своей ленте",
  767. "Announcements": "Объявления",
  768. "Special discussion posts from repositories": "Особые обсуждения из репозиториев",
  769. "Releases": "Выпуски",
  770. "Update posts from repositories": "Новые обновления в репозиториях",
  771. "Sponsors": "Спонсоры",
  772. "Relevant projects or people that are being sponsored": "Проекты или люди, которых кто-то начинает спонсировать",
  773. "Stars": "Звёзды",
  774. "Repositories being starred by people": "Репозитории, которые получают звёзды от людей",
  775. "Repositories": "Репозитории",
  776. "Repositories that are created or forked by people": "Репозитории, созданные или разветвлённые пользователями",
  777. "Repository activity": "Деятельность в репозиториях",
  778. "Issues and pull requests from repositories": "Новые темы и запросы на слияние в репозиториях",
  779. "Follows": "Подписки",
  780. "Who people are following": "На кого подписываются пользователи",
  781. "Recommendations": "Рекомендации",
  782. "Repositories and people you may like": "Репозитории и пользователи, которые могут вам понравиться",
  783. "Include events from starred repositories": "Включать события из репозиториев, на которые вы поставили звезду",
  784. "By default, the feed shows events from repositories you sponsor or watch, and people you follow.": "По умолчанию лента отображает события из репозиториев, которые вы спонсируете или за которыми следите, а также от людей, на которых подписаны.",
  785. "Reset to default": "Сбросить до настроек по умолчанию",
  786. "Save": "Сохранить"
  787. };
  788.  
  789. const elements = document.querySelectorAll(
  790. '.SelectMenu-title, .SelectMenu-item h5, .SelectMenu-item span, .px-3.mt-2 h5, .px-3.mt-2 p'
  791. );
  792.  
  793. elements.forEach(el => {
  794. const text = el.textContent.trim();
  795. if (filterTranslations[text]) {
  796. el.textContent = filterTranslations[text];
  797. }
  798. });
  799. }
  800.  
  801. function translateOpenCopilotMenu() {
  802. document.querySelectorAll('span.prc-ActionList-ItemLabel-TmBhn').forEach(el => {
  803. const originalText = el.textContent.trim();
  804. if (translations[originalText]) {
  805. el.textContent = translations[originalText];
  806. }
  807. });
  808. }
  809.  
  810. function translateStarButtons() {
  811. // Находим все span с классом d-inline внутри кнопок
  812. document.querySelectorAll('.BtnGroup-item .d-inline').forEach(span => {
  813. const text = span.textContent.trim();
  814. if (text === 'Star') {
  815. span.textContent = translations["Star"] || 'Поставить звезду';
  816. } else if (text === 'Starred') {
  817. span.textContent = translations["Starred"] || 'Звезда поставлена';
  818. }
  819. });
  820.  
  821. // Переводим заголовок в диалоговом окне отмены звезды
  822. document.querySelectorAll('.Box-title').forEach(title => {
  823. if (title.textContent.trim() === 'Unstar this repository?') {
  824. title.textContent = translations["Unstar this repository?"] || 'Убрать звезду с этого репозитория?';
  825. }
  826. });
  827.  
  828. // Переводим кнопку Unstar в диалоговом окне
  829. document.querySelectorAll('.btn-danger.btn').forEach(btn => {
  830. if (btn.textContent.trim() === 'Unstar') {
  831. btn.textContent = translations["Unstar"] || 'Убрать звезду';
  832. }
  833. });
  834. }
  835.  
  836. function translateRepositoryButtons() {
  837. // Перевод кнопки Sponsor
  838. document.querySelectorAll('.Button-label .v-align-middle').forEach(span => {
  839. const text = span.textContent.trim();
  840. if (text === 'Sponsor') {
  841. span.textContent = translations["Sponsor"] || 'Спонсировать';
  842. }
  843. });
  844.  
  845. // Перевод кнопки Watch
  846. document.querySelectorAll('.prc-Button-Label-pTQ3x').forEach(span => {
  847. if (span.textContent.trim().startsWith('Watch')) {
  848. // Сохраняем счётчик
  849. const counter = span.querySelector('.Counter');
  850. const counterHTML = counter ? counter.outerHTML : '';
  851.  
  852. // Новый текст с сохранённым счетчиком
  853. span.innerHTML = (translations["Watch"] || 'Следить') +
  854. (counterHTML ? ' ' + counterHTML : '');
  855. }
  856. });
  857.  
  858. // Перевод кнопки Fork
  859. document.querySelectorAll('.BtnGroup.d-flex').forEach(btnGroup => {
  860. // Проверяем, что это непереведенная кнопка Fork
  861. if (btnGroup.textContent.includes('Fork') && !btnGroup.hasAttribute('data-translated-fork')) {
  862. // Сначала сохраним все важные элементы
  863. const counter = btnGroup.querySelector('#repo-network-counter');
  864. const details = btnGroup.querySelector('details');
  865.  
  866. // Создаём функцию для глубокого обхода DOM-дерева
  867. function translateNode(node) {
  868. if (node.nodeType === Node.TEXT_NODE) {
  869. // Регулярное выражение для поиска слова «Fork» с сохранением пробелов
  870. const regex = /(\s*)Fork(\s*)/g;
  871. node.textContent = node.textContent.replace(regex,
  872. (match, before, after) => before + (translations["Fork"] || 'Разветвить') + after);
  873. } else {
  874. // Рекурсивный обход всех дочерних узлов
  875. for (let i = 0; i < node.childNodes.length; i++) {
  876. translateNode(node.childNodes[i]);
  877. }
  878. }
  879. }
  880.  
  881. // Запускаем перевод с корневого элемента
  882. translateNode(btnGroup);
  883.  
  884. // Отмечаем элемент как обработанный
  885. btnGroup.setAttribute('data-translated-fork', 'true');
  886. }
  887. });
  888. }
  889.  
  890. function translateLabelElements() {
  891. document.querySelectorAll('.prc-Label-Label--LG6X').forEach(el => {
  892. if (el.textContent.trim() === 'Free' && translations['Free']) {
  893. el.textContent = translations['Free'];
  894. }
  895. });
  896. }
  897.  
  898. // Функция для перевода статусов тем, кнопок редактирования и создания тем
  899. function translateIssueElements() {
  900. // Перевод статуса «Open» в темах
  901. document.querySelectorAll('span[data-testid="header-state"]').forEach(el => {
  902. if (el.textContent.trim() === 'Open' && translations['Open']) {
  903. // Сохраняем SVG-значок
  904. const svg = el.querySelector('svg');
  905. const svgHTML = svg ? svg.outerHTML : '';
  906.  
  907. // Заменяем текст, сохраняя значок
  908. el.innerHTML = svgHTML + translations['Open'];
  909. }
  910. // Перевод статуса «Closed» в темах
  911. else if (el.textContent.trim() === 'Closed' && translations['Closed']) {
  912. // Сохраняем SVG-значок
  913. const svg = el.querySelector('svg');
  914. const svgHTML = svg ? svg.outerHTML : '';
  915.  
  916. // Заменяем текст, сохраняя значок
  917. el.innerHTML = svgHTML + translations['Closed'];
  918. }
  919. });
  920.  
  921. // Перевод кнопки «Edit»
  922. document.querySelectorAll('.prc-Button-ButtonBase-c50BI .prc-Button-Label-pTQ3x').forEach(el => {
  923. if (el.textContent.trim() === 'Edit' && translations['Edit']) {
  924. el.textContent = translations['Edit'];
  925. }
  926. });
  927.  
  928. // Перевод кнопки «New issue»
  929. document.querySelectorAll('.prc-Button-ButtonBase-c50BI .prc-Button-Label-pTQ3x').forEach(el => {
  930. if (el.textContent.trim() === 'New issue' && translations['New issue']) {
  931. el.textContent = translations['New issue'];
  932. }
  933. });
  934. // Трансформация строки вида «Пользователь opened 2 hours ago» в «Открыта Пользователь 2 часа назад»
  935. document.querySelectorAll('.Box-sc-g0xbh4-0.dqmClk, [data-testid="issue-body-header-author"]').forEach(authorEl => {
  936. // Ищем ближайший родительский контейнер, который содержит также подвал с «opened»
  937. const container = authorEl.closest('.ActivityHeader-module__narrowViewportWrapper--Hjl75, .Box-sc-g0xbh4-0.koxHLL');
  938. if (!container) return;
  939.  
  940. // Находим подвал с текстом «opened»
  941. const footer = container.querySelector('.ActivityHeader-module__footer--FVHp7, .Box-sc-g0xbh4-0.bJQcYY');
  942. if (!footer) return;
  943.  
  944. // Находим span с «opened» и автором
  945. const openedSpan = footer.querySelector('span');
  946. const authorLink = authorEl.querySelector('a[data-testid="issue-body-header-author"], a[href*="/users/"]') || authorEl;
  947.  
  948. // Проверяем, что span содержит «opened»
  949. if (!openedSpan || !openedSpan.textContent.includes('opened')) return;
  950.  
  951. // Получаем ссылку на время с relative-time
  952. const timeLink = footer.querySelector('a[data-testid="issue-body-header-link"]');
  953. if (!timeLink) return;
  954.  
  955. // Находим элемент relative-time внутри ссылки
  956. const relativeTime = timeLink.querySelector('relative-time');
  957. if (!relativeTime) return;
  958.  
  959. try {
  960. // Если уже трансформировано, пропускаем
  961. if (footer.getAttribute('data-ru-transformed')) return;
  962.  
  963. // Отмечаем как трансформированное
  964. footer.setAttribute('data-ru-transformed', 'true');
  965.  
  966. // Создаём новую структуру
  967. // 1. Сохраняем автора
  968. const authorClone = authorLink.cloneNode(true);
  969.  
  970. // 2. Меняем текст в span на перевод «opened» из файла локализации
  971. openedSpan.textContent = translations["opened"] ? translations["opened"] + ' ' : 'Открыта ';
  972.  
  973. // 3. Вставляем автора после слова «Открыта»
  974. openedSpan.after(authorClone);
  975.  
  976. // 4. Добавляем пробел между автором и временем
  977. authorClone.after(document.createTextNode(' '));
  978.  
  979. // 5. Трансформируем текст времени
  980. const originalTimeText = relativeTime.textContent;
  981.  
  982. // Проверяем, содержит ли текст паттерн времени (например, «3 hours ago» или «on Apr 12, 2025»)
  983. const hoursAgoMatch = originalTimeText.match(/(\d+)\s+hours?\s+ago/);
  984. const minutesAgoMatch = originalTimeText.match(/(\d+)\s+minutes?\s+ago/);
  985. const onDateMatch = originalTimeText.match(/on\s+([A-Za-z]+\s+\d+,\s+\d+)/);
  986.  
  987. if (hoursAgoMatch) {
  988. const hours = parseInt(hoursAgoMatch[1], 10);
  989. let translatedText;
  990.  
  991. // Правильное склонение
  992. if (hours === 1) {
  993. translatedText = "Час назад";
  994. } else if (hours >= 2 && hours <= 4) {
  995. translatedText = hours + " часа назад";
  996. } else {
  997. translatedText = hours + " часов назад";
  998. }
  999.  
  1000. relativeTime.textContent = translatedText;
  1001. } else if (minutesAgoMatch) {
  1002. const minutes = parseInt(minutesAgoMatch[1], 10);
  1003. let translatedText;
  1004.  
  1005. // Правильное склонение
  1006. if (minutes === 1) {
  1007. translatedText = "1 минуту назад";
  1008. } else if (minutes >= 2 && minutes <= 4) {
  1009. translatedText = minutes + " минуты назад";
  1010. } else if (minutes >= 5 && minutes <= 20) {
  1011. translatedText = minutes + " минут назад";
  1012. } else {
  1013. // Для чисел 21, 31, 41… используем окончание как для 1
  1014. const lastDigit = minutes % 10;
  1015. const lastTwoDigits = minutes % 100;
  1016.  
  1017. if (lastDigit === 1 && lastTwoDigits !== 11) {
  1018. translatedText = minutes + " минуту назад";
  1019. } else if (lastDigit >= 2 && lastDigit <= 4 && (lastTwoDigits < 10 || lastTwoDigits > 20)) {
  1020. translatedText = minutes + " минуты назад";
  1021. } else {
  1022. translatedText = minutes + " минут назад";
  1023. }
  1024. }
  1025.  
  1026. relativeTime.textContent = translatedText;
  1027. } else if (onDateMatch) {
  1028. // Обрабатываем формат «on Apr 12, 2025»
  1029. // Переводим английское название месяца на русский
  1030. const dateText = onDateMatch[1];
  1031. const monthMapping = {
  1032. Jan: 'января',
  1033. Feb: 'февраля',
  1034. Mar: 'марта',
  1035. Apr: 'апреля',
  1036. May: 'мая',
  1037. Jun: 'июня',
  1038. Jul: 'июля',
  1039. Aug: 'августа',
  1040. Sep: 'сентября',
  1041. Oct: 'октября',
  1042. Nov: 'ноября',
  1043. Dec: 'декабря'
  1044. };
  1045.  
  1046. // Регулярное выражение для поиска и замены месяца в строке даты
  1047. const monthRegex = /([A-Za-z]{3})\s+(\d{1,2}),\s+(\d{4})/;
  1048. const dateMatch = dateText.match(monthRegex);
  1049.  
  1050. if (dateMatch) {
  1051. const monthEn = dateMatch[1];
  1052. const day = dateMatch[2];
  1053. const year = dateMatch[3];
  1054. const monthRu = monthMapping[monthEn] || monthEn;
  1055.  
  1056. relativeTime.textContent = ${day} ${monthRu} ${year}`;
  1057. } else {
  1058. relativeTime.textContent = "в " + dateText;
  1059. }
  1060. }
  1061.  
  1062. // 6. Скрываем оригинальный контейнер с автором
  1063. if (authorEl !== authorLink) {
  1064. authorEl.style.cssText = 'display: none !important;';
  1065. }
  1066.  
  1067. console.log('[Русификатор Гитхаба] Cтрока с автором темы трансформирована');
  1068. } catch (error) {
  1069. console.error('[Русификатор Гитхаба] Ошибка при трансформации строки с автором:', error);
  1070. }
  1071. });
  1072. }
  1073.  
  1074. const feedTitleEl = document.querySelector('[data-target="feed-container.feedTitle"]');
  1075. if (feedTitleEl && window.dashboardHomeTranslation) {
  1076. feedTitleEl.textContent = window.dashboardHomeTranslation;
  1077. }
  1078.  
  1079. document.querySelectorAll('#feed-filter-menu summary').forEach(summary => {
  1080. summary.innerHTML = summary.innerHTML.replace('Filter', translations["Filter"]);
  1081. });
  1082.  
  1083. const observer = new MutationObserver(() => {
  1084. translateTextContent();
  1085. translateAttributes();
  1086. translateCopilotPreview();
  1087. translateTooltips();
  1088. translateGitHubEducation();
  1089. translateExperienceHeaderItems();
  1090. translateEducationExperience();
  1091. translateFilterMenu();
  1092. translateOpenCopilotMenu();
  1093. translateStarButtons();
  1094. translateRepositoryButtons();
  1095. translateLabelElements();
  1096. translateIssueElements();
  1097.  
  1098. // Перевод подвала
  1099. document.querySelectorAll('p.color-fg-subtle.text-small.text-light').forEach(node => {
  1100. if (node.textContent.trim() === '© 2025 GitHub, Inc.') {
  1101. node.textContent = translations['© 2025 GitHub, Inc.'];
  1102. }
  1103. });
  1104.  
  1105. document.querySelectorAll('a.Link').forEach(link => {
  1106. const text = link.textContent.trim();
  1107. if (text === 'About') link.textContent = 'О нас';
  1108. if (text === 'Blog') link.textContent = 'Блог';
  1109. if (text === 'Terms') link.textContent = 'Условия';
  1110. if (text === 'Privacy') link.textContent = 'Конфиденциальность';
  1111. if (text === 'Security') link.textContent = 'Безопасность';
  1112. if (text === 'Status') link.textContent = 'Статус';
  1113. });
  1114.  
  1115. document.querySelectorAll('.Button-label').forEach(btn => {
  1116. if (btn.textContent.trim() === 'Do not share my personal information') {
  1117. btn.textContent = 'Не передавать мои личные данные';
  1118. }
  1119. if (btn.textContent.trim() === 'Manage Cookies') {
  1120. btn.textContent = 'Управление куки';
  1121. }
  1122. });
  1123.  
  1124. // Владельцы и перейти
  1125. document.querySelectorAll('h3.ActionList-sectionDivider-title').forEach(node => {
  1126. if (node.textContent.trim() === 'Owners') {
  1127. node.textContent = translations['Owners'];
  1128. }
  1129. });
  1130.  
  1131. document.querySelectorAll('.ActionListItem-description.QueryBuilder-ListItem-trailing').forEach(span => {
  1132. if (span.textContent.trim() === 'Jump to') {
  1133. span.textContent = translations['Jump to'];
  1134. }
  1135. });
  1136.  
  1137. // Перевод для кнопки «Chat with Copilot»
  1138. document.querySelectorAll('.ActionListItem-label').forEach(el => {
  1139. if (el.textContent.trim() === "Chat with Copilot" && translations["Chat with Copilot"]) {
  1140. el.textContent = translations["Chat with Copilot"];
  1141. }
  1142. });
  1143.  
  1144. // Перевод описания «Start a new Copilot thread»
  1145. document.querySelectorAll('.QueryBuilder-ListItem-trailing').forEach(el => {
  1146. if (el.textContent.trim() === "Start a new Copilot thread" && translations["Start a new Copilot thread"]) {
  1147. el.textContent = translations["Start a new Copilot thread"];
  1148. }
  1149. });
  1150.  
  1151. // Замена «added a repository to»
  1152. document.querySelectorAll('h3.h5.text-normal.color-fg-muted.d-flex.flex-items-center.flex-row.flex-nowrap.width-fit span.flex-1 span.flex-shrink-0').forEach(span => {
  1153. if (span.textContent.trim() === 'added a repository to') {
  1154. span.textContent = translations['added a repository to'];
  1155. }
  1156. });
  1157. });
  1158.  
  1159. // Наблюдение за всем документом, включая изменения атрибутов
  1160. observer.observe(document, {
  1161. childList: true,
  1162. subtree: true,
  1163. attributes: true
  1164. });
  1165.  
  1166. // Будущие изменения
  1167. const observerStarred = new MutationObserver(mutations => {
  1168. mutations.forEach(mutation => {
  1169. mutation.addedNodes.forEach(node => replaceAllStarred(node));
  1170. });
  1171. });
  1172.  
  1173. // Запуск
  1174. observerStarred.observe(document.body, { childList: true, subtree: true });
  1175.  
  1176. // Начальное прохождение
  1177. replaceAllStarred(document.body);
  1178.  
  1179. translateTextContent();
  1180. translateAttributes();
  1181. translateCopilotPreview();
  1182. translateTooltips();
  1183. translateGitHubEducation();
  1184. translateExperienceHeaderItems();
  1185. translateEducationExperience();
  1186. translateFilterMenu();
  1187. translateOpenCopilotMenu();
  1188. translateStarButtons();
  1189. translateRepositoryButtons();
  1190. translateIssueElements();
  1191.  
  1192. // Замена «Filter»
  1193. document.querySelectorAll('summary .octicon-filter').forEach(icon => {
  1194. const summary = icon.parentElement;
  1195. if (summary) {
  1196. summary.childNodes.forEach(node => {
  1197. if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Filter') {
  1198. node.textContent = translations["Filter"];
  1199. }
  1200. });
  1201. }
  1202. });
  1203.  
  1204. // Добавляем текст «Фильтр» в кнопку, если его нет
  1205. document.querySelectorAll('summary').forEach(summary => {
  1206. if (summary.querySelector('.octicon-filter')) {
  1207. let hasFilterText = false;
  1208. summary.childNodes.forEach(node => {
  1209. if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Фильтр') {
  1210. hasFilterText = true;
  1211. }
  1212. });
  1213. if (!hasFilterText) {
  1214. const svgIcon = summary.querySelector('.octicon-filter');
  1215. const textNode = document.createTextNode('Фильтр');
  1216. if (svgIcon && svgIcon.nextSibling) {
  1217. summary.insertBefore(textNode, svgIcon.nextSibling);
  1218. } else {
  1219. summary.appendChild(textNode);
  1220. }
  1221. }
  1222. }
  1223. });
  1224.  
  1225. translateFilterMenu();
  1226. }
  1227. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址