OMC Translator

Load translations for Online Math Contest. / OMCの翻訳を表示します。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         OMC Translator
// @namespace    https://github.com/yuyuuuuuuuuuuuu/omc-translations
// @version      1.1.0
// @description  Load translations for Online Math Contest. / OMCの翻訳を表示します。
// @author       yuyuuuuuuuuuuuu
// @match        https://onlinemathcontest.com/*
// @grant        none
// @homepageURL  https://github.com/yuyuuuuuuuuuuuu/omc-translations
// @license      MIT
// ==/UserScript==
;(function() {
  'use strict'

  const GITHUB_USER = 'yuyuuuuuuuuuuuu'
  const REPO_NAME   = 'omc-translations'
  const BRANCH      = 'main'

  const LANG_KEY = 'omcLang'
  let LANGUAGES = []
  let MESSAGES = {}

  async function loadLanguageConfig() {
    const urlConfig = `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}/languages/config.json`;
    const urlLabel  = `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}/languages/label.json`;
    try {
      const [confRes, labelRes] = await Promise.all([
        fetch(urlConfig), fetch(urlLabel)
      ]);
      const conf   = await confRes.json();   // { languages: ["en", ...] }
      const labels = await labelRes.json();  // { en: "English 🇺🇸", ja: "日本語 🇯🇵 original", ... }

      // 日本語を最初に、以降翻訳対象を順に
      LANGUAGES = [{ code: 'ja', label: labels['ja'] || '日本語' }];
      for (const code of conf.languages) {
        if (code !== 'ja') {
          LANGUAGES.push({ code, label: labels[code] || code });
        }
      }
    } catch (e) {
      console.error('Language config の読み込みに失敗:', e);
      // フォールバック: 日本語と英語のみ
      LANGUAGES = [
        { code: 'ja', label: '日本語' },
        { code: 'en', label: 'English 🇺🇸' }
      ];
    }
  }

  async function loadMessages() {
    if (getLang() === 'ja') return;
    const urlMsg = `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}` +
                   `/languages/${getLang()}/static/messages.json`;
    try {
      MESSAGES = await fetch(urlMsg).then(r => r.json());
    } catch (e) {
      console.warn('messages.json の読み込みに失敗:', e);
      MESSAGES = {};
    }
  }

  function getLang() {
    const v = localStorage.getItem(LANG_KEY);
    return LANGUAGES.some(l => l.code === v) ? v : 'ja';
  }
  function setLang(code) {
    localStorage.setItem(LANG_KEY, code);
  }

  function addLangDropdown() {
    const ul = document.querySelector('.navbar-nav.mr-auto');
    if (!ul) return;
    const current = getLang();
    const li = document.createElement('li');
    li.className = 'nav-item dropdown';
    li.style.marginLeft = '10px';

    const toggle = document.createElement('a');
    toggle.className = 'nav-link dropdown-toggle';
    toggle.href = '#';
    toggle.id = 'omcLangDropdown';
    toggle.setAttribute('role', 'button');
    toggle.setAttribute('data-toggle', 'dropdown');
    toggle.textContent = `Language: ${LANGUAGES.find(l => l.code === current).label}`;

    const menu = document.createElement('div');
    menu.className = 'dropdown-menu';
    menu.setAttribute('aria-labelledby', 'omcLangDropdown');

    LANGUAGES.forEach(l => {
      const a = document.createElement('a');
      a.className = 'dropdown-item';
      a.href = '#';
      a.textContent = l.label;
      if (l.code === current) a.style.fontWeight = 'bold';
      a.addEventListener('click', e => {
        e.preventDefault();
        setLang(l.code);
        location.reload();
      });
      menu.appendChild(a);
    });

    li.appendChild(toggle);
    li.appendChild(menu);
    ul.appendChild(li);
  }

  async function translateStaticUI() {
    if (getLang() === 'ja') return;
    const base = `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}` +
                 `/languages/${getLang()}/static`;
    let config;
    try {
      config = await fetch(`${base}/config.json`).then(r => r.json());
    } catch (e) {
      console.error('config.json の取得に失敗:', e);
      return;
    }

    const path = location.pathname;
    const entries = config.filter(c =>
      c.paths.some(p => new RegExp(`^${p}$`).test(path))
    );
    if (!entries.length) return;

    const dictNames = [...new Set(entries.flatMap(e => e.dictionaries))];
    const dict = {};
    for (const name of dictNames) {
      try {
        const d = await fetch(`${base}/${name}.json`).then(r => r.json());
        Object.assign(dict, d);
      } catch (e) {
        console.warn(`辞書 ${name}.json の読み込みに失敗:`, e);
      }
    }

    const walker = document.createTreeWalker(
      document.body, NodeFilter.SHOW_TEXT, null, false
    );
    let node;
    while (node = walker.nextNode()) {
      // ——— 動的コンテンツ (#problem_content, #editorial_content) は除外 ———
      if (node.parentElement.closest('#problem_content, #editorial_content')) {
        continue;
      }

      let text = node.nodeValue;
      if (!text.trim()) continue;
      text = text.replace(/[\u00A0\u3000]/g, ' ');
      let replaced = text;
      for (const [ja, en] of Object.entries(dict)) {
        const key = ja.replace(/[\u00A0\u3000]/g, ' ');
        if (key && replaced.includes(key)) {
          replaced = replaced.split(key).join(en);
        }
      }
      if (replaced !== text) {
        node.nodeValue = replaced;
      }
    }
  }

  function parseUserEditorial() {
    const m = location.pathname.match(
      /^\/contests\/([^\/]+)\/editorial\/(\d+)\/(\d+)(?:\/|$)/
    );
    return m ? { contestId: m[1], taskId: m[2], userId: m[3] } : null;
  }

  function rawUrl(type, contestId, id) {
    return `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}` +
           `/languages/${getLang()}/contests/${contestId}/${type}/${id}.html`;
  }

  function appendMessage(container, text, color) {
    const p = document.createElement('p');
    p.textContent = text;
    p.style.color = color;
    p.style.marginTop = '1em';
    container.appendChild(p);
  }

  function replaceTasks() {
    const m = location.pathname.match(
      /^\/contests\/([^\/]+)\/tasks\/(\d+)(?:\/$|$)/
    );
    if (!m || getLang() === 'ja') return;
    const c = document.getElementById('problem_content');
    fetch(rawUrl('tasks', m[1], m[2]))
      .then(r => { if (!r.ok) throw 0; return r.text(); })
      .then(html => {
        if (!c) return;
        c.innerHTML = html;
        // 注意書きを追加
        if (MESSAGES.tasks) {
          appendMessage(c, MESSAGES.tasks, 'blue');
        }
      })
      .catch(() => {
        if (c && MESSAGES.tasks_not_done) {
          appendMessage(c, MESSAGES.tasks_not_done, 'orange');
        }
      });
  }

  function replaceEditorial() {
    const m = location.pathname.match(
      /^\/contests\/([^\/]+)\/editorial\/(\d+)(?:\/$|$)/
    );
    if (!m || getLang() === 'ja' || parseUserEditorial()) return;
    const c = document.getElementById('editorial_content');
    fetch(rawUrl('editorial', m[1], m[2]))
      .then(r => { if (!r.ok) throw 0; return r.text(); })
      .then(html => {
        if (!c) return;
        c.innerHTML = html;
        // 注意書きを追加
        if (MESSAGES.editorials) {
          appendMessage(c, MESSAGES.editorials, 'blue');
        }
      })
      .catch(() => {
        if (c && MESSAGES.editorial_not_done) {
          appendMessage(c, MESSAGES.editorial_not_done, 'orange');
        }
      });
  }

  function replaceUserEditorial() {
    const info = parseUserEditorial();
    if (!info || getLang() === 'ja') return;
    const c = document.getElementById('editorial_content');
    fetch(rawUrl('user_editorial', info.contestId, info.userId))
      .then(r => { if (!r.ok) throw 0; return r.text(); })
      .then(html => {
        if (!c) return;
        c.innerHTML = html;
        if (MESSAGES.user_editorial) {
          appendMessage(c, MESSAGES.user_editorial, 'blue');
        }
      })
      .catch(() => {
        if (c && MESSAGES.user_editorial_not_done) {
          appendMessage(c, MESSAGES.user_editorial_not_done, 'orange');
        }
      });
  }

  async function main() {
    await loadLanguageConfig();
    addLangDropdown();
    await loadMessages();
    await translateStaticUI();
    replaceTasks();
    replaceUserEditorial();
    replaceEditorial();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', main);
  } else {
    main();
  }

})();