OMC Translator

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

// ==UserScript==
// @name         OMC Translator
// @namespace    https://github.com/yuyuuuuuuuuuuuu/omc-translations
// @version      1.0.0
// @description  Load translations for Online Math Contest problems and editorials. / 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',
          REPO_NAME   = 'omc-translations',
          BRANCH      = 'main';

    // 対応言語リスト (code: 言語コード, label: ハンバーグ表示)
    const LANGUAGES = [
        { code: 'en', label: 'English 🇺🇸' },
        { code: 'ja', label: '日本語 🇯🇵' }
        // 追加例(誰か2ドルくれ): { code: 'zh', label: '中文 🇨🇳' }
    ];

    // デフォルト 'en'
    const getLang = () => {
        const v = localStorage.getItem('omcLang');
        return LANGUAGES.some(l => l.code === v) ? v : 'en';
    };
    const setLang = code => localStorage.setItem('omcLang', code);

    // URL から contestId/taskId を抽出 (type: 'tasks' or 'editorial')
    const parseInfo = type => {
        const re = new RegExp(`^/contests/([^/]+)/(?:${type})/(\\d+)(?:/|$)`);
        const m = location.pathname.match(re);
        return m ? { contestId: m[1], taskId: m[2] } : null;
    };

    // GitHub raw URL を生成(type: 'tasks' or 'editorial', lang, contestId, taskId)
    const rawUrl = (type, lang, c, t) =>
        `https://raw.githubusercontent.com/${GITHUB_USER}/${REPO_NAME}/${BRANCH}` +
        `/languages/${lang}/contests/${c}/${type}/${t}.html`;

    // 言語ハンバーグをヘッダーに追加
    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 = 'langDropdown';
        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', 'langDropdown');

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

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

    //('problem_content' or 'editorial_content') 置き換え
    function replaceContent(type) {
        const info = parseInfo(type);
        if (!info || getLang() === 'ja') return;

        const { contestId, taskId } = info;
        const url = rawUrl(type, getLang(), contestId, taskId);

        fetch(url)
            .then(res => {
                if (!res.ok) throw new Error('not found');
                return res.text();
            })
            .then(html => {
                const sel = type === 'tasks' ? 'problem_content' : 'editorial_content';
                const container = document.getElementById(sel);
                if (container) container.innerHTML = html;
            })
            .catch(() => {
                const sel = type === 'tasks' ? 'problem_content' : 'editorial_content';
                const c = document.getElementById(sel);
                if (c) {
                    const p = document.createElement('p');
                    p.textContent = "It seems the translation hasn't been completed yet... Please wait a little longer...";
                    p.style.color = 'red';
                    p.style.marginTop = '1em';
                    c.appendChild(p);
                }
            });
    }

    //  実行 
    const main = () => {
        addLangDropdown();
        replaceContent('tasks');
        replaceContent('editorial');
    };

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

})();