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-B24
  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 = {
  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. return `${day} ${monthRu} ${year}, ${hourStr}:${minute} по московскому времени`;
  113. }
  114. return text;
  115. }
  116.  
  117. // Функция для перевода элементов <relative-time>
  118. function translateRelativeTimes() {
  119. const timeElements = document.querySelectorAll('relative-time');
  120. timeElements.forEach(el => {
  121. // Если элемент уже переведён, можно добавить атрибут data-translated
  122. if (el.getAttribute('data-translated')) return;
  123.  
  124. // Перевод всплывающей подсказки, если атрибут title существует
  125. if (el.hasAttribute('title')) {
  126. const originalTitle = el.getAttribute('title');
  127. el.setAttribute('title', translateAbsoluteTime(originalTitle));
  128. }
  129. // Отмечаем элемент как переведённый
  130. el.setAttribute('data-translated', 'true');
  131. });
  132. }
  133.  
  134. // функция для проверки, находится ли элемент в контейнере, где перевод нежелателен
  135. function isExcludedElement(el) {
  136. // Если элемент находится внутри заголовков Markdown, то не переводим
  137. if (el.closest('.markdown-heading')) return true;
  138. // Если элемент находится внутри ячейки с названием каталога, то не переводим
  139. if (el.closest('.react-directory-filename-column')) return true;
  140. return false;
  141. }
  142.  
  143. // Функция для перевода блока GitHub Education
  144. function translateEducationExperience() {
  145. document.querySelectorAll('.experience__action-item h3').forEach(el => {
  146. if (el.textContent.includes('Add') && el.textContent.includes('repositories to a list')) {
  147. el.innerHTML = el.innerHTML.replace(
  148. /Add .* repositories to a list/,
  149. 'Добавьте репозитории, на которые поставили звезду, в список'
  150. );
  151. }
  152. });
  153.  
  154. document.querySelectorAll('.experience__action-item p.mt-4.f4').forEach(el => {
  155. if (el.textContent.includes('To complete this task, create a list with at least 3')) {
  156. el.innerHTML = el.innerHTML.replace(
  157. /To complete this task, create a list with at least 3 .* repos with the list name 'My Repo Watchlist'./,
  158. 'Чтобы выполнить это задание, создайте список, содержащий не менее трёх репозиториев, на которые вы поставили звезду, с названием «My Repo Watchlist».'
  159. );
  160. }
  161. });
  162.  
  163. document.querySelectorAll('.experience__actions-items-labels .Label--attention').forEach(el => {
  164. if (el.textContent.trim() === 'Incomplete') {
  165. el.textContent = 'Не выполнено';
  166. }
  167. });
  168.  
  169. document.querySelectorAll('.experience__actions-items-labels .Label--secondary').forEach(el => {
  170. if (el.textContent.trim() === 'List') {
  171. el.textContent = 'Список';
  172. }
  173. });
  174.  
  175. document.querySelectorAll('.experience__cta .Button--primary .Button-label').forEach(el => {
  176. if (el.textContent.trim() === 'See detailed instructions') {
  177. el.textContent = 'Подробные инструкции';
  178. }
  179. });
  180.  
  181. document.querySelectorAll('.experience__cta .Button--secondary .Button-label').forEach(el => {
  182. if (el.textContent.trim() === 'Mark complete') {
  183. el.textContent = 'Отметить как выполненное';
  184. }
  185. });
  186. }
  187.  
  188. function translateTextContent() {
  189. const elements = document.querySelectorAll(
  190. '.ActionList-sectionDivider-title, .ActionListItem-label, span[data-content], ' +
  191. '.AppHeader-context-item-label, #qb-input-query, .Truncate-text, h2, button, ' +
  192. '.Label, a, img[alt], .Box-title, .post__content p, .post__content li, .Button-label, ' +
  193. '.prc-ActionList-GroupHeading-eahp0'
  194. );
  195.  
  196. elements.forEach(el => {
  197. // Если элемент подпадает под исключения, пропускаем его
  198. if (isExcludedElement(el)) return;
  199.  
  200. if (el.tagName === 'IMG' && el.alt.trim() in translations) {
  201. el.alt = translations[el.alt.trim()];
  202. } else if (el.childElementCount === 0) {
  203. const text = el.textContent.trim();
  204. if (translations[text]) {
  205. el.textContent = translations[text];
  206. } else {
  207. const match = text.match(/^(\d+) repositories$/);
  208. if (match) {
  209. const count = parseInt(match[1], 10);
  210. el.textContent = getRepositoriesTranslation(count);
  211. } else if (text === 'added a repository to') {
  212. el.textContent = translations['added a repository to'];
  213. }
  214. }
  215. } else {
  216. // Часть функции translateTextContent(), отвечающая за обработку элементов с дочерними элементами
  217. if (el.childElementCount > 0) {
  218. // Сборка текстового содержания с учётом дочерних элементов
  219. let text = '';
  220. el.childNodes.forEach(node => {
  221. if (node.nodeType === Node.TEXT_NODE) {
  222. text += node.textContent;
  223. } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'KBD') {
  224. text += '/'; // Добавление символа «/» из <kbd>
  225. }
  226. });
  227. text = text.trim();
  228. if (translations[text]) {
  229. // Создание нового фрагмента с переводом и сохранение тега <kbd>, если перевод соответствует шаблону
  230. const newFragment = document.createDocumentFragment();
  231. const parts = translations[text].split('<kbd class="AppHeader-search-kbd">/</kbd>');
  232. newFragment.append(document.createTextNode(parts[0]));
  233. // Добавлять тег <kbd> только если есть вторая часть перевода
  234. if (parts.length > 1 && parts[1] !== undefined) {
  235. const kbd = document.createElement('kbd');
  236. kbd.className = 'AppHeader-search-kbd';
  237. kbd.textContent = '/';
  238. newFragment.append(kbd);
  239. newFragment.append(document.createTextNode(parts[1]));
  240. }
  241. // Очистка элемента и вставка нового контента
  242. el.innerHTML = '';
  243. el.appendChild(newFragment);
  244. }
  245. el.childNodes.forEach(node => {
  246. if (node.nodeType === Node.TEXT_NODE) {
  247. const originalText = node.textContent;
  248. const trimmed = originalText.trim();
  249. if (translations[trimmed]) {
  250. node.textContent = translations[trimmed];
  251. } else if (originalText.includes("starred")) {
  252. node.textContent = originalText.replace("starred", translations["starred"]);
  253. } else if (originalText.includes("added a repository to")) {
  254. node.textContent = originalText.replace("added a repository to", translations['added a repository to']);
  255. } else if (originalText.includes("Notifications")) {
  256. node.textContent = originalText.replace("Notifications", translations['Notifications']);
  257. }
  258. }
  259. });
  260. // Последный выхват строчек
  261. if (/\bstarred\b/.test(el.innerHTML)) {
  262. el.innerHTML = el.innerHTML.replace(/\bstarred\b/g, translations["starred"]);
  263. }
  264. if (/\badded a repository to\b/.test(el.innerHTML)) {
  265. el.innerHTML = el.innerHTML.replace(/\badded a repository to\b/g, translations['added a repository to']);
  266. }
  267. if (/\bNotifications\b/.test(el.innerHTML)) {
  268. el.innerHTML = el.innerHTML.replace(/\bNotifications\b/g, translations['Notifications']);
  269. }
  270. } else {
  271. // Сначала каждый узел
  272. el.childNodes.forEach(node => {
  273. if (node.nodeType === Node.TEXT_NODE) {
  274. let originalText = node.textContent;
  275. // Переводы
  276. originalText = originalText.replace(/\bstarred\b/g, translations["starred"]);
  277. originalText = originalText.replace(/\badded a repository to\b/g, translations['added a repository to']);
  278. originalText = originalText.replace(/\bNotifications\b/g, translations['Notifications']);
  279. node.textContent = originalText;
  280. }
  281. });
  282.  
  283. // Если всё ещё остаётся, заменить внутренний HTML
  284. if (/\bstarred\b/.test(el.innerHTML)) {
  285. el.innerHTML = el.innerHTML.replace(/\bstarred\b/g, translations["starred"]);
  286. }
  287. if (/\badded a repository to\b/.test(el.innerHTML)) {
  288. el.innerHTML = el.innerHTML.replace(/\badded a repository to\b/g, translations['added a repository to']);
  289. }
  290. if (/\bNotifications\b/.test(el.innerHTML)) {
  291. el.innerHTML = el.innerHTML.replace(/\bNotifications\b/g, translations['Notifications']);
  292. }
  293. }
  294. }
  295. });
  296. formatStarCount();
  297. translateRelativeTimes();
  298.  
  299. document.querySelectorAll('.Button-label').forEach(btn => {
  300. if (btn.textContent.trim() === "New") {
  301. btn.textContent = "Создать";
  302. }
  303. });
  304. }
  305.  
  306. function translateCopilotPreview() {
  307. const askCopilotPlaceholder = document.querySelector('.copilotPreview__input[placeholder="Ask Copilot"]');
  308. if (askCopilotPlaceholder && translations['Ask Copilot']) {
  309. askCopilotPlaceholder.setAttribute('placeholder', translations['Ask Copilot']);
  310. }
  311. document.querySelectorAll('.copilotPreview__suggestionButton div').forEach(div => {
  312. const text = div.textContent.trim();
  313. if (translations[text]) {
  314. div.innerHTML = translations[text];
  315. }
  316. });
  317. }
  318.  
  319. function translateAttributes() {
  320. // Перевод placeholder
  321. document.querySelectorAll('input[placeholder]').forEach(el => {
  322. const text = el.getAttribute('placeholder');
  323. if (translations[text]) {
  324. el.setAttribute('placeholder', translations[text]);
  325. }
  326. });
  327. // Перевод aria-label
  328. document.querySelectorAll('[aria-label]').forEach(el => {
  329. const text = el.getAttribute('aria-label');
  330. if (translations[text]) {
  331. el.setAttribute('aria-label', translations[text]);
  332. }
  333. });
  334. }
  335.  
  336. function translateTooltips() {
  337. const copilotChatTooltip = document.querySelector('tool-tip[for="copilot-chat-header-button"]');
  338. if (copilotChatTooltip && copilotChatTooltip.textContent.trim() === 'Chat with Copilot') {
  339. // Используем перевод из dashboard, сохранённый ранее
  340. copilotChatTooltip.textContent = window.dashboardCopilotTranslation;
  341. }
  342.  
  343. document.querySelectorAll('tool-tip[role="tooltip"]').forEach(tooltip => {
  344. const text = tooltip.textContent.trim();
  345. if (translations[text]) {
  346. tooltip.textContent = translations[text];
  347. }
  348. });
  349.  
  350. document.querySelectorAll('.prc-TooltipV2-Tooltip-cYMVY').forEach(tooltip => {
  351. const text = tooltip.textContent.trim();
  352. if (translations[text]) {
  353. tooltip.textContent = translations[text];
  354. }
  355. });
  356. }
  357.  
  358. function translateGitHubEducation() {
  359. const noticeForms = document.querySelectorAll('div.js-notice form.js-notice-dismiss');
  360.  
  361. noticeForms.forEach(form => {
  362. const heading = form.querySelector('h3.h4');
  363. if (heading && heading.textContent.trim() === 'Learn. Collaborate. Grow.') {
  364. heading.textContent = 'Учитесь. Кооперируйтесь. Развивайтесь.';
  365. }
  366.  
  367. const desc = form.querySelector('p.my-3.text-small');
  368. if (desc && desc.textContent.includes('GitHub Education gives you the tools')) {
  369. desc.textContent = 'GitHub Education предоставляет инструменты и поддержку сообщества, чтобы вы могли принимать технологические вызовы и превращать их в возможности. Ваше технологическое будущее начинается здесь!';
  370. }
  371.  
  372. const link = form.querySelector('.Button-label');
  373. if (link && link.textContent.trim() === 'Go to GitHub Education') {
  374. link.textContent = 'Перейти в GitHub Education';
  375. }
  376. });
  377.  
  378. document.querySelectorAll('.h5.color-fg-on-emphasis.text-mono').forEach(el => {
  379. if (el.textContent.trim() === 'LaunchPad') {
  380. el.textContent = translations['LaunchPad'];
  381. }
  382. });
  383.  
  384. document.querySelectorAll('.experience__gradient.experience__title').forEach(el => {
  385. if (el.textContent.trim() === 'Intro to GitHub') {
  386. el.textContent = translations['Intro to GitHub'];
  387. }
  388. });
  389.  
  390. // Шрифт Inter
  391. const educationNavBlock = document.querySelector('.d-flex.flex-justify-center.flex-md-justify-start.pb-5.pb-sm-7');
  392. if (educationNavBlock) {
  393. educationNavBlock.style.fontFamily = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  394. }
  395.  
  396. document.querySelectorAll('header h1.mb-5').forEach(el => {
  397. el.style.fontFamily = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  398. });
  399. }
  400.  
  401. function translateExperienceHeaderItems() {
  402. // Перевод заголовков и текста в блоке experience__header__items
  403. document.querySelectorAll('.experience__header__item .experience__hero__heading').forEach(el => {
  404. const text = el.textContent.trim();
  405. if (translations[text]) {
  406. el.textContent = translations[text];
  407. }
  408. });
  409.  
  410. // Перевод основного текста в блоке experience__header__items
  411. document.querySelectorAll('.experience__header__item p.color-fg-on-emphasis').forEach(el => {
  412. const text = el.textContent.trim();
  413. if (translations[text]) {
  414. el.textContent = translations[text];
  415. }
  416. });
  417.  
  418. // Шрифт Inter ко всему блоку для лучшей поддержки кириллицы
  419. document.querySelectorAll('.color-fg-on-emphasis').forEach(el => {
  420. el.style.fontFamily = 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  421. });
  422. }
  423.  
  424. function translateFilterMenu() {
  425. const filterTranslations = {
  426. "Filter": "Фильтр",
  427. "Events": "События",
  428. "Activity you want to see on your feed": "Деятельность, которую вы хотите видеть в своей ленте",
  429. "Announcements": "Объявления",
  430. "Special discussion posts from repositories": "Особые обсуждения из репозиториев",
  431. "Releases": "Выпуски",
  432. "Update posts from repositories": "Новые обновления в репозиториях",
  433. "Sponsors": "Спонсоры",
  434. "Relevant projects or people that are being sponsored": "Проекты или люди, которых кто-то начинает спонсировать",
  435. "Stars": "Звёзды",
  436. "Repositories being starred by people": "Репозитории, которые получают звёзды от людей",
  437. "Repositories": "Репозитории",
  438. "Repositories that are created or forked by people": "Репозитории, созданные или разветвлённые пользователями",
  439. "Repository activity": "Деятельность в репозиториях",
  440. "Issues and pull requests from repositories": "Новые темы и запросы на слияние в репозиториях",
  441. "Follows": "Подписки",
  442. "Who people are following": "На кого подписываются пользователи",
  443. "Recommendations": "Рекомендации",
  444. "Repositories and people you may like": "Репозитории и пользователи, которые могут вам понравиться",
  445. "Include events from starred repositories": "Включать события из репозиториев, на которые вы поставили звезду",
  446. "By default, the feed shows events from repositories you sponsor or watch, and people you follow.": "По умолчанию лента отображает события из репозиториев, которые вы спонсируете или за которыми следите, а также от людей, на которых подписаны.",
  447. "Reset to default": "Сбросить до настроек по умолчанию",
  448. "Save": "Сохранить"
  449. };
  450.  
  451. const elements = document.querySelectorAll(
  452. '.SelectMenu-title, .SelectMenu-item h5, .SelectMenu-item span, .px-3.mt-2 h5, .px-3.mt-2 p'
  453. );
  454.  
  455. elements.forEach(el => {
  456. const text = el.textContent.trim();
  457. if (filterTranslations[text]) {
  458. el.textContent = filterTranslations[text];
  459. }
  460. });
  461. }
  462.  
  463. function translateOpenCopilotMenu() {
  464. document.querySelectorAll('span.prc-ActionList-ItemLabel-TmBhn').forEach(el => {
  465. const originalText = el.textContent.trim();
  466. if (translations[originalText]) {
  467. el.textContent = translations[originalText];
  468. }
  469. });
  470. }
  471.  
  472. function translateStarButtons() {
  473. // Находим все span с классом d-inline внутри кнопок
  474. document.querySelectorAll('.BtnGroup-item .d-inline').forEach(span => {
  475. const text = span.textContent.trim();
  476. if (text === 'Star') {
  477. span.textContent = translations["Star"] || 'Поставить звезду';
  478. } else if (text === 'Starred') {
  479. span.textContent = translations["Starred"] || 'Звезда поставлена';
  480. }
  481. });
  482.  
  483. // Переводим заголовок в диалоговом окне отмены звезды
  484. document.querySelectorAll('.Box-title').forEach(title => {
  485. if (title.textContent.trim() === 'Unstar this repository?') {
  486. title.textContent = translations["Unstar this repository?"] || 'Убрать звезду с этого репозитория?';
  487. }
  488. });
  489.  
  490. // Переводим кнопку Unstar в диалоговом окне
  491. document.querySelectorAll('.btn-danger.btn').forEach(btn => {
  492. if (btn.textContent.trim() === 'Unstar') {
  493. btn.textContent = translations["Unstar"] || 'Убрать звезду';
  494. }
  495. });
  496. }
  497.  
  498. function translateRepositoryButtons() {
  499. // Перевод кнопки Sponsor
  500. document.querySelectorAll('.Button-label .v-align-middle').forEach(span => {
  501. const text = span.textContent.trim();
  502. if (text === 'Sponsor') {
  503. span.textContent = translations["Sponsor"] || 'Спонсировать';
  504. }
  505. });
  506.  
  507. // Перевод кнопки Watch
  508. document.querySelectorAll('.prc-Button-Label-pTQ3x').forEach(span => {
  509. if (span.textContent.trim().startsWith('Watch')) {
  510. // Сохраняем счётчик
  511. const counter = span.querySelector('.Counter');
  512. const counterHTML = counter ? counter.outerHTML : '';
  513.  
  514. // Новый текст с сохранённым счетчиком
  515. span.innerHTML = (translations["Watch"] || 'Следить') +
  516. (counterHTML ? ' ' + counterHTML : '');
  517. }
  518. });
  519.  
  520. // Перевод кнопки Fork
  521. document.querySelectorAll('.BtnGroup.d-flex').forEach(btnGroup => {
  522. // Проверяем, что это непереведенная кнопка Fork
  523. if (btnGroup.textContent.includes('Fork') && !btnGroup.hasAttribute('data-translated-fork')) {
  524. // Сначала сохраним все важные элементы
  525. const counter = btnGroup.querySelector('#repo-network-counter');
  526. const details = btnGroup.querySelector('details');
  527.  
  528. // Создаём функцию для глубокого обхода DOM-дерева
  529. function translateNode(node) {
  530. if (node.nodeType === Node.TEXT_NODE) {
  531. // Регулярное выражение для поиска слова «Fork» с сохранением пробелов
  532. const regex = /(\s*)Fork(\s*)/g;
  533. node.textContent = node.textContent.replace(regex,
  534. (match, before, after) => before + (translations["Fork"] || 'Разветвить') + after);
  535. } else {
  536. // Рекурсивный обход всех дочерних узлов
  537. for (let i = 0; i < node.childNodes.length; i++) {
  538. translateNode(node.childNodes[i]);
  539. }
  540. }
  541. }
  542.  
  543. // Запускаем перевод с корневого элемента
  544. translateNode(btnGroup);
  545.  
  546. // Отмечаем элемент как обработанный
  547. btnGroup.setAttribute('data-translated-fork', 'true');
  548. }
  549. });
  550. }
  551.  
  552. function translateLabelElements() {
  553. document.querySelectorAll('.prc-Label-Label--LG6X').forEach(el => {
  554. if (el.textContent.trim() === 'Free' && translations['Free']) {
  555. el.textContent = translations['Free'];
  556. }
  557. });
  558. }
  559.  
  560. const feedTitleEl = document.querySelector('[data-target="feed-container.feedTitle"]');
  561. if (feedTitleEl && window.dashboardHomeTranslation) {
  562. feedTitleEl.textContent = window.dashboardHomeTranslation;
  563. }
  564.  
  565. document.querySelectorAll('#feed-filter-menu summary').forEach(summary => {
  566. summary.innerHTML = summary.innerHTML.replace('Filter', translations["Filter"]);
  567. });
  568.  
  569. const observer = new MutationObserver(() => {
  570. translateTextContent();
  571. translateAttributes();
  572. translateCopilotPreview();
  573. translateTooltips();
  574. translateGitHubEducation();
  575. translateExperienceHeaderItems();
  576. translateEducationExperience();
  577. translateFilterMenu();
  578. translateOpenCopilotMenu();
  579. translateStarButtons();
  580. translateRepositoryButtons();
  581. translateLabelElements();
  582.  
  583. // Перевод подвала
  584. document.querySelectorAll('p.color-fg-subtle.text-small.text-light').forEach(node => {
  585. if (node.textContent.trim() === '© 2025 GitHub, Inc.') {
  586. node.textContent = translations['© 2025 GitHub, Inc.'];
  587. }
  588. });
  589.  
  590. document.querySelectorAll('a.Link').forEach(link => {
  591. const text = link.textContent.trim();
  592. if (text === 'About') link.textContent = 'О нас';
  593. if (text === 'Blog') link.textContent = 'Блог';
  594. if (text === 'Terms') link.textContent = 'Условия';
  595. if (text === 'Privacy') link.textContent = 'Конфиденциальность';
  596. if (text === 'Security') link.textContent = 'Безопасность';
  597. if (text === 'Status') link.textContent = 'Статус';
  598. });
  599.  
  600. document.querySelectorAll('.Button-label').forEach(btn => {
  601. if (btn.textContent.trim() === 'Do not share my personal information') {
  602. btn.textContent = 'Не передавать мои личные данные';
  603. }
  604. if (btn.textContent.trim() === 'Manage Cookies') {
  605. btn.textContent = 'Управление куки';
  606. }
  607. });
  608.  
  609. // Владельцы и перейти
  610. document.querySelectorAll('h3.ActionList-sectionDivider-title').forEach(node => {
  611. if (node.textContent.trim() === 'Owners') {
  612. node.textContent = translations['Owners'];
  613. }
  614. });
  615.  
  616. document.querySelectorAll('.ActionListItem-description.QueryBuilder-ListItem-trailing').forEach(span => {
  617. if (span.textContent.trim() === 'Jump to') {
  618. span.textContent = translations['Jump to'];
  619. }
  620. });
  621.  
  622. // Перевод для кнопки «Chat with Copilot»
  623. document.querySelectorAll('.ActionListItem-label').forEach(el => {
  624. if (el.textContent.trim() === "Chat with Copilot" && translations["Chat with Copilot"]) {
  625. el.textContent = translations["Chat with Copilot"];
  626. }
  627. });
  628.  
  629. // Перевод описания «Start a new Copilot thread»
  630. document.querySelectorAll('.QueryBuilder-ListItem-trailing').forEach(el => {
  631. if (el.textContent.trim() === "Start a new Copilot thread" && translations["Start a new Copilot thread"]) {
  632. el.textContent = translations["Start a new Copilot thread"];
  633. }
  634. });
  635.  
  636. // Замена «added a repository to»
  637. 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 => {
  638. if (span.textContent.trim() === 'added a repository to') {
  639. span.textContent = translations['added a repository to'];
  640. }
  641. });
  642. });
  643.  
  644. // Наблюдение за всем документом, включая изменения атрибутов
  645. observer.observe(document, {
  646. childList: true,
  647. subtree: true,
  648. attributes: true
  649. });
  650.  
  651. // Будущие изменения
  652. const observerStarred = new MutationObserver(mutations => {
  653. mutations.forEach(mutation => {
  654. mutation.addedNodes.forEach(node => replaceAllStarred(node));
  655. });
  656. });
  657.  
  658. // Запуск
  659. observerStarred.observe(document.body, { childList: true, subtree: true });
  660.  
  661. // Начальное прохождение
  662. replaceAllStarred(document.body);
  663.  
  664. translateTextContent();
  665. translateAttributes();
  666. translateCopilotPreview();
  667. translateTooltips();
  668. translateGitHubEducation();
  669. translateExperienceHeaderItems();
  670. translateEducationExperience();
  671. translateFilterMenu();
  672. translateOpenCopilotMenu();
  673. translateStarButtons();
  674. translateRepositoryButtons();
  675.  
  676. // Замена «Filter»
  677. document.querySelectorAll('summary .octicon-filter').forEach(icon => {
  678. const summary = icon.parentElement;
  679. if (summary) {
  680. summary.childNodes.forEach(node => {
  681. if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Filter') {
  682. node.textContent = translations["Filter"];
  683. }
  684. });
  685. }
  686. });
  687.  
  688. // Добавляем текст «Фильтр» в кнопку, если его нет
  689. document.querySelectorAll('summary').forEach(summary => {
  690. if (summary.querySelector('.octicon-filter')) {
  691. let hasFilterText = false;
  692. summary.childNodes.forEach(node => {
  693. if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Фильтр') {
  694. hasFilterText = true;
  695. }
  696. });
  697. if (!hasFilterText) {
  698. const svgIcon = summary.querySelector('.octicon-filter');
  699. const textNode = document.createTextNode('Фильтр');
  700. if (svgIcon && svgIcon.nextSibling) {
  701. summary.insertBefore(textNode, svgIcon.nextSibling);
  702. } else {
  703. summary.appendChild(textNode);
  704. }
  705. }
  706. }
  707. });
  708.  
  709. translateFilterMenu();
  710. }
  711. })();