OMC Translator

Load translations for Online Math Contest problems and editorials. / OMCの問題文および公式解説文の翻訳を表示します。

当前为 2025-06-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name OMC Translator
  3. // @namespace https://github.com/yuyuuuuuuuuuuuu/omc-translations
  4. // @version 1.0.0
  5. // @description Load translations for Online Math Contest problems and editorials. / OMCの問題文および公式解説文の翻訳を表示します。
  6. // @author yuyuuuuuuuuuuuu
  7. // @match https://onlinemathcontest.com/*
  8. // @grant none
  9. // @homepageURL https://github.com/yuyuuuuuuuuuuuu/omc-translations
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // 設定
  17. const GITHUB_USER = 'yuyuuuuuuuuuuuu',
  18. REPO_NAME = 'omc-translations',
  19. BRANCH = 'main';
  20.  
  21. // 対応言語リスト (code: 言語コード, label: ハンバーグ表示)
  22. const LANGUAGES = [
  23. { code: 'en', label: 'English 🇺🇸' },
  24. { code: 'ja', label: '日本語 🇯🇵' }
  25. // 追加例(誰か2ドルくれ): { code: 'zh', label: '中文 🇨🇳' }
  26. ];
  27.  
  28. // デフォルト 'en'
  29. const getLang = () => {
  30. const v = localStorage.getItem('omcLang');
  31. return LANGUAGES.some(l => l.code === v) ? v : 'en';
  32. };
  33. const setLang = code => localStorage.setItem('omcLang', code);
  34.  
  35. // URL から contestId/taskId を抽出 (type: 'tasks' or 'editorial')
  36. const parseInfo = type => {
  37. const re = new RegExp(`^/contests/([^/]+)/(?:${type})/(\\d+)(?:/|$)`);
  38. const m = location.pathname.match(re);
  39. return m ? { contestId: m[1], taskId: m[2] } : null;
  40. };
  41.  
  42. // GitHub raw URL を生成(type: 'tasks' or 'editorial', lang, contestId, taskId)
  43. const rawUrl = (type, lang, c, t) =>
  44. `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}` +
  45. `/languages/${lang}/contests/${c}/${type}/${t}.html`;
  46.  
  47. // 言語ハンバーグをヘッダーに追加
  48. function addLangDropdown() {
  49. const ul = document.querySelector('.navbar-nav.mr-auto');
  50. if (!ul) return;
  51.  
  52. const current = getLang();
  53. const li = document.createElement('li');
  54. li.className = 'nav-item dropdown';
  55. li.style.marginLeft = '10px';
  56.  
  57. const toggle = document.createElement('a');
  58. toggle.className = 'nav-link dropdown-toggle';
  59. toggle.href = '#';
  60. toggle.id = 'langDropdown';
  61. toggle.setAttribute('role', 'button');
  62. toggle.setAttribute('data-toggle', 'dropdown');
  63. toggle.textContent = `Language: ${LANGUAGES.find(l => l.code === current).label}`;
  64.  
  65. const menu = document.createElement('div');
  66. menu.className = 'dropdown-menu';
  67. menu.setAttribute('aria-labelledby', 'langDropdown');
  68.  
  69. LANGUAGES.forEach(l => {
  70. const a = document.createElement('a');
  71. a.className = 'dropdown-item';
  72. a.href = '#';
  73. a.dataset.code = l.code;
  74. a.textContent = l.label;
  75. if (l.code === current) a.style.fontWeight = 'bold';
  76. a.addEventListener('click', e => {
  77. e.preventDefault();
  78. setLang(l.code);
  79. toggle.textContent = `Language: ${l.label}`;
  80. menu.classList.remove('show');
  81. location.reload();
  82. });
  83. menu.appendChild(a);
  84. });
  85.  
  86. li.appendChild(toggle);
  87. li.appendChild(menu);
  88. ul.appendChild(li);
  89. }
  90.  
  91. //('problem_content' or 'editorial_content') 置き換え
  92. function replaceContent(type) {
  93. const info = parseInfo(type);
  94. if (!info || getLang() === 'ja') return;
  95.  
  96. const { contestId, taskId } = info;
  97. const url = rawUrl(type, getLang(), contestId, taskId);
  98.  
  99. fetch(url)
  100. .then(res => {
  101. if (!res.ok) throw new Error('not found');
  102. return res.text();
  103. })
  104. .then(html => {
  105. const sel = type === 'tasks' ? 'problem_content' : 'editorial_content';
  106. const container = document.getElementById(sel);
  107. if (container) container.innerHTML = html;
  108. })
  109. .catch(() => {
  110. const sel = type === 'tasks' ? 'problem_content' : 'editorial_content';
  111. const c = document.getElementById(sel);
  112. if (c) {
  113. const p = document.createElement('p');
  114. p.textContent = "It seems the translation hasn't been completed yet... Please wait a little longer...";
  115. p.style.color = 'red';
  116. p.style.marginTop = '1em';
  117. c.appendChild(p);
  118. }
  119. });
  120. }
  121.  
  122. // 実行
  123. const main = () => {
  124. addLangDropdown();
  125. replaceContent('tasks');
  126. replaceContent('editorial');
  127. };
  128.  
  129. if (document.readyState === 'loading') {
  130. document.addEventListener('DOMContentLoaded', main);
  131. } else {
  132. main();
  133. }
  134.  
  135. })();